diff --git a/build/tasks/eslint.js b/build/tasks/eslint.js index bfc0f2d100..f5cb803189 100644 --- a/build/tasks/eslint.js +++ b/build/tasks/eslint.js @@ -9,11 +9,14 @@ const eslint = require('gulp-eslint'); gulp.task('lint', () => { return gulp.src([ '**/*.js', + 'fabric-network/**/*.js', 'fabric-client/**/*.js', 'fabric-ca-client/lib/*.js', + '!fabric-network/coverage/**', '!fabric-ca-client/coverage/**', '!test/typescript/*.js', '!node_modules/**', + '!fabric-network/node_modules/**', '!fabric-client/node_modules/**', '!fabric-ca-client/node_modules/**', '!docs/**', diff --git a/build/tasks/test.js b/build/tasks/test.js index 9c3f76c3ab..36159f402d 100644 --- a/build/tasks/test.js +++ b/build/tasks/test.js @@ -67,6 +67,7 @@ process.env.THIRDPARTY_IMG_TAG = thirdpartyImageTag; gulp.task('pre-test', () => { return gulp.src([ + 'fabric-network/lib/**/*.js', 'fabric-client/lib/**/*.js', 'fabric-ca-client/lib/FabricCAClientImpl.js', 'fabric-ca-client/lib/helper.js', @@ -133,14 +134,21 @@ gulp.task('mocha-fabric-client', } ); -gulp.task('run-test', ['run-full', 'mocha-fabric-client'], +gulp.task('mocha-fabric-network', + () => { + return gulp.src(['./fabric-network/test/**/*.js'], { read: false }) + .pipe(mocha({ reporter: 'list', exit: true })); + } +); + +gulp.task('run-test', ['run-full', 'mocha-fabric-client', 'mocha-fabric-network'], () => { return gulp.src(['./fabric-ca-client/test/**/*.js'], { read: false }) .pipe(mocha({ reporter: 'list', exit: true })); } ); -gulp.task('run-test-headless', ['run-headless', 'mocha-fabric-client'], +gulp.task('run-test-headless', ['run-headless', 'mocha-fabric-client', 'mocha-fabric-network'], () => { return gulp.src(['./fabric-ca-client/test/**/*.js'], { read: false }) .pipe(mocha({ reporter: 'list', exit: true })); @@ -163,6 +171,7 @@ gulp.task('run-full', ['clean-up', 'lint', 'pre-test', 'compile', 'docker-ready' '!test/unit/logger.js', // channel: mychannel, chaincode: e2enodecc:v0 'test/integration/nodechaincode/e2e.js', + 'test/integration/network-e2e/e2e.js', // channel: mychannel, chaincode: end2endnodesdk:v0/v1 'test/integration/e2e.js', 'test/integration/query.js', diff --git a/fabric-network/README.md b/fabric-network/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/fabric-network/index.js b/fabric-network/index.js new file mode 100644 index 0000000000..2bb5c294b3 --- /dev/null +++ b/fabric-network/index.js @@ -0,0 +1,9 @@ +/** + * Copyright 2018 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +module.exports.Network = require('./lib/network'); +module.exports.InMemoryWallet = require('./lib/impl/wallet/inmemorywallet'); +module.exports.X509WalletMixin = require('./lib/impl/wallet/x509walletmixin'); diff --git a/fabric-network/lib/api/wallet.js b/fabric-network/lib/api/wallet.js new file mode 100644 index 0000000000..e23d693bcc --- /dev/null +++ b/fabric-network/lib/api/wallet.js @@ -0,0 +1,48 @@ +/** + * Copyright 2018 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +class Wallet { + + // =============================================== + // SPI Methods + // =============================================== + + async setUserContext(client, label) { + throw new Error('Not implemented'); + } + + async configureClientStores(client, label) { + throw new Error('Not implemented'); + } + + //========================================================= + // End user APIs + //========================================================= + + async import(label, identity) { + throw new Error('Not implemented'); + } + + async export(label) { + throw new Error('Not implemented'); + } + + async list() { + throw new Error('Not implemented'); + } + + async delete(label) { + throw new Error('Not implemented'); + } + + async exists(label) { + throw new Error('Not implemented'); + } +} + +module.exports = Wallet; diff --git a/fabric-network/lib/channel.js b/fabric-network/lib/channel.js new file mode 100644 index 0000000000..d9e37579ca --- /dev/null +++ b/fabric-network/lib/channel.js @@ -0,0 +1,169 @@ +/** + * Copyright 2018 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; +const FabricConstants = require('fabric-client/lib/Constants'); +const Contract = require('./contract'); +const logger = require('./logger').getLogger('channel.js'); +const util = require('util'); + +class Channel { + + /** + * Channel constructor for internal use only + * @param network + * @param channel + * @private + */ + constructor(network, channel) { + logger.debug('in Channel constructor'); + + this.network = network; + this.channel = channel; + this.contracts = new Map(); + this.initialized = false; + } + + /** + * create a map of mspId's and the channel peers in those mspIds + * @private + * @memberof Network + */ + _mapPeersToMSPid() { + logger.debug('in _mapPeersToMSPid'); + + // TODO: assume 1-1 mapping of mspId to org as the node-sdk makes that assumption + // otherwise we would need to find the channel peer in the network config collection or however SD + // stores things + + const peerMap = new Map(); + const channelPeers = this.channel.getPeers(); + + // bug in service discovery, peers don't have the associated mspid + if (channelPeers.length > 0) { + for (const channelPeer of channelPeers) { + const mspId = channelPeer.getMspid(); + if (mspId) { + let peerList = peerMap.get(mspId); + if (!peerList) { + peerList = []; + peerMap.set(mspId, peerList); + } + peerList.push(channelPeer); + } + } + } + if (peerMap.size === 0) { + const msg = 'no suitable peers associated with mspIds were found'; + logger.error('_mapPeersToMSPid: ' + msg); + throw new Error(msg); + } + return peerMap; + } + + /** + * initialize the channel if it hasn't been done + * @private + */ + async _initializeInternalChannel() { + logger.debug('in _initializeInternalChannel'); + + //TODO: Should this work across all peers or just orgs peers ? + //TODO: should sort peer list to the identity org initializing the channel. + //TODO: Candidate to push to low level node-sdk. + + const ledgerPeers = this.channel.getPeers().filter((cPeer) => { + return cPeer.isInRole(FabricConstants.NetworkConfig.LEDGER_QUERY_ROLE); + }); + + if (ledgerPeers.length === 0) { + const msg = 'no suitable peers available to initialize from'; + logger.error('_initializeInternalChannel: ' + msg); + throw new Error(msg); + } + + let ledgerPeerIndex = 0; + let success = false; + + while (!success) { + try { + const initOptions = { + target: ledgerPeers[ledgerPeerIndex] + }; + + await this.channel.initialize(initOptions); + success = true; + } catch(error) { + if (ledgerPeerIndex >= ledgerPeers.length - 1) { + const msg = util.format('Unable to initialize channel. Attempted to contact %j Peers. Last error was %s', ledgerPeers.length, error); + logger.error('_initializeInternalChannel: ' + msg); + throw new Error(msg); + } + ledgerPeerIndex++; + } + } + } + + async _initialize() { + logger.debug('in initialize'); + + if (this.initialized) { + return; + } + + await this._initializeInternalChannel(); + this.peerMap = this._mapPeersToMSPid(); + + this.initialized = true; + } + + getInternalChannel() { + logger.debug('in getInternalChannel'); + + return this.channel; + } + + getPeerMap() { + logger.debug('in getPeerMap'); + + return this.peerMap; + } + + /** + * Returns an instance of a contract (chaincode) on the current channel + * @param chaincodeId + * @returns {Contract} + * @api + */ + getContract(chaincodeId) { + logger.debug('in getContract'); + + // check initialized flag + // Create the new Contract + let contract = this.contracts.get(chaincodeId); + if (!contract) { + contract = new Contract( + this.channel, + chaincodeId, + this.network + ); + } + return contract; + } + + _dispose() { + logger.debug('in _dispose'); + + // Danger as this cached in network, and also async so how would + // channel._dispose() followed by channel.initialize() be safe ? + // make this private is the safest option. + this.contracts.clear(); + this.initialized = false; + } + +} + +module.exports = Channel; \ No newline at end of file diff --git a/fabric-network/lib/contract.js b/fabric-network/lib/contract.js new file mode 100644 index 0000000000..58c9963dcc --- /dev/null +++ b/fabric-network/lib/contract.js @@ -0,0 +1,151 @@ +/** + * Copyright 2018 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const logger = require('./logger').getLogger('contract.js'); +const util = require('util'); + +class Contract { + + constructor(channel, chaincodeId, network) { + logger.debug('in Contract constructor'); + + this.channel = channel; + this.chaincodeId = chaincodeId; + this.network = network; + + } + + /** + * Check for proposal response errors. + * @private + * @param {any} responses the responses from the install, instantiate or invoke + * @return {Object} number of ignored errors and valid responses + * @throws if there are no valid responses at all. + */ + _validatePeerResponses(responses) { + logger.debug('in _validatePeerResponses'); + + if (!responses.length) { + logger.error('_validatePeerResponses: No results were returned from the request'); + throw new Error('No results were returned from the request'); + } + + const validResponses = []; + const invalidResponses = []; + const invalidResponseMsgs = []; + + responses.forEach((responseContent) => { + if (responseContent instanceof Error) { + const warning = util.format('Response from attempted peer comms was an error: %j', responseContent); + logger.warn('_validatePeerResponses: ' + warning); + invalidResponseMsgs.push(warning); + invalidResponses.push(responseContent); + } else { + + // not an error, if it is from a proposal, verify the response + if (!this.channel.verifyProposalResponse(responseContent)) { + // the node-sdk doesn't provide any external utilities from parsing the responseContent. + // there are internal ones which may do what is needed or we would have to decode the + // protobufs ourselves but it should really be the node sdk doing this. + const warning = util.format('Proposal response from peer failed verification: %j', responseContent.response); + logger.warn('_validatePeerResponses: ' + warning); + invalidResponseMsgs.push(warning); + invalidResponses.push(responseContent); + } else if (responseContent.response.status !== 200) { + const warning = util.format('Unexpected response of %j. Payload was: %j', responseContent.response.status, responseContent.response.payload); + logger.warn('_validatePeerResponses: ' + warning); + invalidResponseMsgs.push(warning); + } else { + validResponses.push(responseContent); + } + } + }); + + if (validResponses.length === 0) { + const errorMessages = [ 'No valid responses from any peers.' ]; + invalidResponseMsgs.forEach(invalidResponse => errorMessages.push(invalidResponse)); + const msg = errorMessages.join('\n'); + logger.error('_validatePeerResponses: ' + msg); + throw new Error(msg); + } + + return {validResponses, invalidResponses, invalidResponseMsgs}; + } + + /** + * @param {string} transactionName Transaction function name + * @param {...string} parameters Transaction function parameters + * @returns {Buffer} Payload response + */ + async submitTransaction(transactionName, ...parameters) { + logger.debug('in submitTransaction: ' + transactionName); + + // check parameters + if(typeof transactionName !== 'string' || transactionName.length === 0) { + const msg = util.format('transactionName must be a non-empty string: %j', transactionName); + logger.error('submitTransaction: ' + msg); + throw new Error(msg); + } + parameters.forEach((parameter) => { + if(typeof parameter !== 'string') { + const msg = util.format('transaction parameters must be strings: %j', parameter); + logger.error('submitTransaction: ' + msg); + throw new Error(msg); + } + }); + + const txId = this.network.getClient().newTransactionID(); + + // Submit the transaction to the endorsers. + const request = { + chaincodeId: this.chaincodeId, + txId, + fcn: transactionName, + args: parameters + }; + + // node sdk will target all peers on the channel that are endorsingPeer or do something special for a discovery environment + const results = await this.channel.sendTransactionProposal(request); + const proposalResponses = results[0]; + + //TODO: what to do about invalidResponses + const {validResponses} = this._validatePeerResponses(proposalResponses); + if (validResponses.length === 0) { + //TODO: include the invalidResponsesMsgs ? + const msg = 'No valid responses from any peers'; + logger.error('submitTransaction: ' + msg); + throw new Error(msg); + } + + // Submit the endorsed transaction to the primary orderers. + const proposal = results[1]; + + //TODO: more to do regarding checking the response (see hlfconnection.invokeChaincode) + + const response = await this.channel.sendTransaction({ + proposalResponses: validResponses, + proposal + }); + + if (response.status !== 'SUCCESS') { + const msg = util.format('Failed to send peer responses for transaction \'%j\' to orderer. Response status: %j', txId.getTransactionID(), response.status); + logger.error('submitTransaction: ' + msg); + throw new Error(msg); + } + + // return the payload from the invoked chaincode + let result = null; + if (validResponses[0].response.payload && validResponses[0].response.payload.length > 0) { + result = validResponses[0].response.payload; + } + return result; + + } +} + +module.exports = Contract; \ No newline at end of file diff --git a/fabric-network/lib/impl/wallet/basewallet.js b/fabric-network/lib/impl/wallet/basewallet.js new file mode 100644 index 0000000000..b948d5bf5f --- /dev/null +++ b/fabric-network/lib/impl/wallet/basewallet.js @@ -0,0 +1,184 @@ +/** + * Copyright 2018 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const Client = require('fabric-client'); +const X509WalletMixin = require('./x509walletmixin'); +const Wallet = require('../../api/wallet'); +const logger = require('../../logger').getLogger('network.js'); +const util = require('util'); + +class BaseWallet extends Wallet { + + constructor(walletMixin = new X509WalletMixin()) { + super(); + logger.debug(util.format('in BaseWallet constructor, mixin = %O', walletMixin)); + this.storesInitialized = false; + this.walletMixin = walletMixin; + } + + // =============================================== + // SPI Methods + // =============================================== + + /** + * End users of a wallet don't make use of this method, this method is for use by the + * fabric-network implementation + * + * @param {*} client + * @param {*} label + * @returns + * @memberof Wallet + */ + async setUserContext(client, label) { + logger.debug(util.format('in setUserContext, label = %s', label)); + + label = this.normalizeLabel(label); + + //TODO: We could check the client to see if the context matches what we would load ? + //Although this may be complex to do, maybe we could cache the previous label and + //Another setUserContext call can be bypassed. + await this.configureClientStores(client, label); + const loadedIdentity = await client.getUserContext(label, true); + if (!loadedIdentity || !loadedIdentity.isEnrolled()) { + const msg = util.format('identity \'%s\' isn\'t enrolled, or loaded', label); + logger.error('setUserContext: ' + msg); + throw new Error(msg); + } + return loadedIdentity; + } + + async configureClientStores(client, label) { + logger.debug(util.format('in configureClientStores, label = %s', label)); + + label = this.normalizeLabel(label); + if (!client) { + client = new Client(); + } + + const store = await this.getStateStore(label); + client.setStateStore(store); + + let cryptoSuite; + if (this.walletMixin && this.walletMixin.getCryptoSuite) { + cryptoSuite = await this.walletMixin.getCryptoSuite(label, this); + } + + if (!cryptoSuite) { + cryptoSuite = await this.getCryptoSuite(label); + } + client.setCryptoSuite(cryptoSuite); + return client; + } + + //======================================== + // The following 2 apis are implemented to + // provide the persistence mechanism + // a mixin can override the getCryptoSuite + //======================================== + + async getStateStore(label) { + throw new Error('Not implemented'); + } + + async getCryptoSuite(label) { + throw new Error('Not implemented'); + } + + // if this is overridden, then it has to be bi-directional + // for the list to work properly. + normalizeLabel(label) { + return label; + } + + //========================================================= + // End user APIs + //========================================================= + + //========================================================= + // Mixins provide support for import & export + //========================================================= + + async import(label, identity) { + logger.debug(util.format('in import, label = %s', label)); + + label = this.normalizeLabel(label); + const client = await this.configureClientStores(null, label); + if (this.walletMixin && this.walletMixin.importIdentity) { + return await this.walletMixin.importIdentity(client, label, identity); + } else { + logger.error('no import method exists'); + throw new Error('no import method exists'); + } + } + + async export(label) { + logger.debug(util.format('in export, label = %s', label)); + + label = this.normalizeLabel(label); + const client = await this.configureClientStores(null, label); + if (this.walletMixin && this.walletMixin.exportIdentity) { + return await this.walletMixin.exportIdentity(client, label); + } else { + logger.error('no export method exists'); + throw new Error('no export method exists'); + } + } + + //========================================================= + // Wallets combined with mixins provide support for list + //========================================================= + + async list() { + logger.debug('in list'); + + const idInfoList = []; + const labelList = await this.getAllLabels(); // these need to be denormalised + if (labelList && labelList.length > 0 && this.walletMixin && this.walletMixin.getIdentityInfo) { + for (const label of labelList) { + const client = await this.configureClientStores(null, label); + const idInfo = await this.walletMixin.getIdentityInfo(client, label); + if (idInfo) { + idInfoList.push(idInfo); + } + else { + idInfoList.push({ + label, + mspId: 'not provided', + identifier: 'not provided' + }); + } + } + } + + logger.debug(util.format('list returns %j', idInfoList)); + return idInfoList; + } + + async getAllLabels() { + return null; + } + + //========================================================= + // Wallets provide support for delete and exists + //========================================================= + + + async delete(label) { + throw new Error('Not implemented'); + } + + async exists(label) { + throw new Error('Not implemented'); + } + + //TODO: FUTURE: Need some sort of api for a mixin to call to be able to integrate correctly + //with the specific persistence mechanism if it wants to use the same persistence + //feature +} + +module.exports = BaseWallet; diff --git a/fabric-network/lib/impl/wallet/inmemorywallet.js b/fabric-network/lib/impl/wallet/inmemorywallet.js new file mode 100644 index 0000000000..07bf60eaac --- /dev/null +++ b/fabric-network/lib/impl/wallet/inmemorywallet.js @@ -0,0 +1,96 @@ +/** + * Copyright 2018 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const Client = require('fabric-client'); +const BaseWallet = require('./basewallet'); +const api = require('fabric-client/lib/api.js'); +const logger = require('../../logger').getLogger('network.js'); +const util = require('util'); + +// this will be shared across all instance of a memory wallet, so really an app should +// only have one instance otherwise if you put 2 different identities with the same +// label it will overwrite the existing one. +const memoryStore = new Map(); + +class InMemoryWallet extends BaseWallet { + constructor(walletmixin) { + super(walletmixin); + logger.debug('in InMemoryWallet constructor'); + } + + async getStateStore(label) { + logger.debug(util.format('in getStateStore, label = %s', label)); + label = this.normalizeLabel(label); + const store = await new InMemoryKVS(label); + return store; + } + + async getCryptoSuite(label) { + logger.debug(util.format('in getCryptoSuite, label = %s', label)); + label = this.normalizeLabel(label); + const cryptoSuite = Client.newCryptoSuite(); + cryptoSuite.setCryptoKeyStore(Client.newCryptoKeyStore(InMemoryKVS, label)); + return cryptoSuite; + } + + async delete(label) { + logger.debug(util.format('in delete, label = %s', label)); + label = this.normalizeLabel(label); + memoryStore.delete(label); + } + + async exists(label) { + logger.debug(util.format('in exists, label = %s', label)); + label = this.normalizeLabel(label); + return memoryStore.has(label); + } + + async getAllLabels() { + const labels = Array.from(memoryStore.keys()); + logger.debug(util.format('getAllLabels returns: %j', labels)); + return labels; + } +} + +class InMemoryKVS extends api.KeyValueStore { + + /** + * constructor + * + * @param {Object} options contains a single property path which points to the top-level directory + * for the store + */ + constructor(prefix) { + super(); + logger.debug('in InMemoryKVS constructor, prefix = ' + prefix); + this.partitionKey = prefix; + return Promise.resolve(this); + } + + async getValue(name) { + logger.debug('getValue, name = ' + name); + const idStore = memoryStore.get(this.partitionKey); + if (!idStore) { + return null; + } + return idStore.get(name); + } + + async setValue(name, value) { + logger.debug('setValue, name = ' + name); + let idStore = memoryStore.get(this.partitionKey); + if (!idStore) { + idStore = new Map(); + } + idStore.set(name, value); + memoryStore.set(this.partitionKey, idStore); + return value; + } +} + +module.exports = InMemoryWallet; \ No newline at end of file diff --git a/fabric-network/lib/impl/wallet/x509walletmixin.js b/fabric-network/lib/impl/wallet/x509walletmixin.js new file mode 100644 index 0000000000..c0021d2c6b --- /dev/null +++ b/fabric-network/lib/impl/wallet/x509walletmixin.js @@ -0,0 +1,69 @@ +/** + * Copyright 2018 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const logger = require('../../logger').getLogger('network.js'); +const util = require('util'); + +class X509WalletMixin { + + static createIdentity(mspId, certificate, privateKey) { + logger.debug('in createIdentity: mspId = ' + mspId); + return { + type: 'X509', + mspId, + certificate, + privateKey + }; + } + + async importIdentity(client, label, identity) { + logger.debug(util.format('in importIdentity, label = %s', label)); + // check identity type + const cryptoContent = { + signedCertPEM: identity.certificate, + privateKeyPEM: identity.privateKey + }; + + await client.createUser( + { + username: label, + mspid: identity.mspId, + cryptoContent: cryptoContent + }); + } + + async exportIdentity(client, label) { + logger.debug(util.format('in exportIdentity, label = %s', label)); + const user = await client.getUserContext(label, true); + let result = null; + if (user) { + result = X509WalletMixin.createIdentity( + user._mspId, + user.getIdentity()._certificate, + user.getSigningIdentity()._signer._key.toBytes() + ); + } + return result; + } + + async getIdentityInfo(client, label) { + logger.debug(util.format('in getIdentityInfo, label = %s', label)); + const user = await client.getUserContext(label, true); + let result = null; + if (user) { + result = { + label, + mspId: user._mspId, + identifier: user.getIdentity()._publicKey.getSKI() + }; + } + return result; + } +} + +module.exports = X509WalletMixin; \ No newline at end of file diff --git a/fabric-network/lib/logger.js b/fabric-network/lib/logger.js new file mode 100644 index 0000000000..632ef80bc8 --- /dev/null +++ b/fabric-network/lib/logger.js @@ -0,0 +1,12 @@ +/** + * Copyright 2018 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +// reuse the client implementation of the logger as we are part of the client +// abstracted out in case we want to change this in the future. +const sdkUtils = require('fabric-client/lib/utils'); +module.exports.getLogger = sdkUtils.getLogger; \ No newline at end of file diff --git a/fabric-network/lib/network.js b/fabric-network/lib/network.js new file mode 100644 index 0000000000..c580e416c0 --- /dev/null +++ b/fabric-network/lib/network.js @@ -0,0 +1,145 @@ +/** + * Copyright 2018 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const Client = require('fabric-client'); +const Channel = require('./channel'); +const logger = require('./logger').getLogger('network.js'); + +class Network { + + static _mergeOptions(defaultOptions, suppliedOptions) { + for (const prop in suppliedOptions) { + if (suppliedOptions[prop] instanceof Object && prop.endsWith('Options')) { + if (defaultOptions[prop] === undefined) { + defaultOptions[prop] = suppliedOptions[prop]; + } else { + Network._mergeOptions(defaultOptions[prop], suppliedOptions[prop]); + } + } else { + defaultOptions[prop] = suppliedOptions[prop]; + } + } + } + + /** + * Public constructor for Network object + */ + constructor() { + logger.debug('in Network constructor'); + this.client = null; + this.wallet = null; + this.channels = new Map(); + + // default options + this.options = { + commitTimeout: 300 * 1000, + }; + } + + /** + * Initialize the network with a connection profile + * + * @param {*} ccp + * @param {*} options + * @memberof Network + */ + async initialize(ccp, options) { + logger.debug('in initialize'); + + if (!options || !options.wallet) { + logger.error('initialize: A wallet must be assigned to a Network instance'); + throw new Error('A wallet must be assigned to a Network instance'); + } + + Network._mergeOptions(this.options, options); + + if (!(ccp instanceof Client)) { + // still use a ccp for the discovery peer and ca information + logger.debug('initialize: loading client from ccp'); + this.client = Client.loadFromConfig(ccp); + } else { + // initialize from an existing Client object instance + logger.debug('initialize: using existing client object'); + this.client = ccp; + } + + // setup an initial identity for the network + if (options.identity) { + logger.debug('initialize: setting identity'); + this.currentIdentity = await options.wallet.setUserContext(this.client, options.identity); + } + } + + /** + * Get the current identity + * + * @returns + * @memberof Network + */ + getCurrentIdentity() { + logger.debug('in getCurrentIdentity'); + return this.currentIdentity; + } + + /** + * Get the underlying Client object instance + * + * @returns + * @memberof Network + */ + getClient() { + logger.debug('in getClient'); + return this.client; + } + + /** + * Returns the set of options associated with the network connection + * @returns {{commitTimeout: number}|*} + * @memberOf Network + */ + getOptions() { + logger.debug('in getOptions'); + return this.options; + } + + /** + * clean up this network in prep for it to be discarded and garbage collected + * + * @memberof Network + */ + dispose() { + logger.debug('in cleanup'); + for (const channel of this.channels.values()) { + channel._dispose(); + } + this.channels.clear(); + } + + /** + * Returns an object representing the channel + * @param channelName + * @returns {Promise} + * @memberOf Network + */ + async getChannel(channelName) { + logger.debug('in getChannel'); + const existingChannel = this.channels.get(channelName); + if (!existingChannel) { + logger.debug('getChannel: create channel object and initialize'); + const channel = this.client.getChannel(channelName); + const newChannel = new Channel(this, channel); + await newChannel._initialize(); + this.channels.set(channelName, newChannel); + return newChannel; + } + return existingChannel; + } +} + + +module.exports = Network; \ No newline at end of file diff --git a/fabric-network/package.json b/fabric-network/package.json new file mode 100644 index 0000000000..6812098639 --- /dev/null +++ b/fabric-network/package.json @@ -0,0 +1,47 @@ +{ + "name": "fabric-network", + "version": "1.3.0-snapshot", + "main": "index.js", + "repository": { + "type": "gerrit", + "url": "https://gerrit.hyperledger.org/r/#/admin/projects/fabric-sdk-node" + }, + "homepage": "https://www.hyperledger.org/projects/fabric", + "author": { + "name": "hyperledger/fabric", + "email": "fabric@lists.hyperledger.org" + }, + "scripts": { + "test": "gulp test-headless" + }, + "dependencies": { + "fabric-ca-client": "file:../fabric-ca-client", + "fabric-client": "file:../fabric-client", + "nano": "^6.4.4", + "rimraf": "^2.6.2", + "uuid": "^3.2.1" + }, + "devDependencies": { + "nyc": "^11.8.0", + "rewire": "^4.0.1", + "sinon": "^5.0.7" + }, + "nyc": { + "include": [], + "reporter": [ + "lcov", + "json", + "text", + "text-summary", + "cobertura" + ], + "cache": true + }, + "license": "Apache-2.0", + "licenses": [ + { + "type": "Apache-2.0", + "url": "https://github.com/hyperledger/fabric/blob/master/LICENSE" + } + ] +} diff --git a/fabric-network/test/api/wallet.js b/fabric-network/test/api/wallet.js new file mode 100644 index 0000000000..c51c01d1c3 --- /dev/null +++ b/fabric-network/test/api/wallet.js @@ -0,0 +1,47 @@ +/** + * Copyright 2018 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +'use strict'; +const chai = require('chai'); +chai.use(require('chai-as-promised')); + +const Wallet = require('../../lib/api/wallet'); + + +describe('Wallet', () => { + const wallet = new Wallet(); + + it('throws exception calling setUserContext()', () => { + return wallet.setUserContext(null, null).should.be.rejectedWith('Not implemented'); + }); + + it('throws exception calling configureClientStores()', () => { + return wallet.configureClientStores(null, null).should.be.rejectedWith('Not implemented'); + }); + + it('throws exception calling import()', () => { + return wallet.import(null, null).should.be.rejectedWith('Not implemented'); + }); + + it('throws exception calling export()', () => { + return wallet.export(null).should.be.rejectedWith('Not implemented'); + }); + + it('throws exception calling list()', () => { + return wallet.list().should.be.rejectedWith('Not implemented'); + }); + + it('throws exception calling delete()', () => { + return wallet.delete(null).should.be.rejectedWith('Not implemented'); + }); + + it('throws exception calling exists()', () => { + return wallet.exists(null).should.be.rejectedWith('Not implemented'); + }); + + +}); \ No newline at end of file diff --git a/fabric-network/test/channel.js b/fabric-network/test/channel.js new file mode 100644 index 0000000000..2603667d96 --- /dev/null +++ b/fabric-network/test/channel.js @@ -0,0 +1,224 @@ +/** + * Copyright 2018 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; +const sinon = require('sinon'); +const rewire = require('rewire'); + +const InternalChannel = rewire('fabric-client/lib/Channel'); +const Peer = InternalChannel.__get__('ChannelPeer'); +const Client = require('fabric-client'); +const TransactionID = require('fabric-client/lib/TransactionID.js'); +const FABRIC_CONSTANTS = require('fabric-client/lib/Constants'); + +const chai = require('chai'); +chai.use(require('chai-as-promised')); + +const Channel = require('../lib/channel'); +const Network = require('../lib/network'); +const Contract = require('../lib/contract'); + + +describe('Channel', () => { + + let sandbox = sinon.createSandbox(); + let clock; + + let mockChannel, mockClient; + let mockPeer1, mockPeer2, mockPeer3; + let channel; + let mockTransactionID, mockNetwork; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + mockChannel = sinon.createStubInstance(InternalChannel); + mockClient = sinon.createStubInstance(Client); + mockTransactionID = sinon.createStubInstance(TransactionID); + mockTransactionID.getTransactionID.returns('00000000-0000-0000-0000-000000000000'); + mockClient.newTransactionID.returns(mockTransactionID); + mockChannel.getName.returns('testchainid'); + + mockPeer1 = sinon.createStubInstance(Peer); + mockPeer1.index = 1; // add these so that the mockPeers can be distiguished when used in WithArgs(). + mockPeer1.getName.returns('Peer1'); + + mockPeer2 = sinon.createStubInstance(Peer); + mockPeer2.index = 2; + mockPeer2.getName.returns('Peer2'); + + mockPeer3 = sinon.createStubInstance(Peer); + mockPeer3.index = 3; + mockPeer3.getName.returns('Peer3'); + + mockNetwork = sinon.createStubInstance(Network); + mockNetwork.getOptions.returns({useDiscovery: false}); + channel = new Channel(mockNetwork, mockChannel); + + }); + + afterEach(() => { + sandbox.restore(); + clock.restore(); + }); + + + describe('#_initializeInternalChannel', () => { + let peerArray; + let mockPeer4, mockPeer5; + beforeEach(() => { + mockPeer4 = sinon.createStubInstance(Peer); + mockPeer4.index = 4; + mockPeer5 = sinon.createStubInstance(Peer); + mockPeer5.index = 5; + + mockPeer1.isInRole.withArgs(FABRIC_CONSTANTS.NetworkConfig.LEDGER_QUERY_ROLE).returns(true); + mockPeer2.isInRole.withArgs(FABRIC_CONSTANTS.NetworkConfig.LEDGER_QUERY_ROLE).returns(false); + mockPeer3.isInRole.withArgs(FABRIC_CONSTANTS.NetworkConfig.LEDGER_QUERY_ROLE).returns(true); + mockPeer4.isInRole.withArgs(FABRIC_CONSTANTS.NetworkConfig.LEDGER_QUERY_ROLE).returns(true); + mockPeer5.isInRole.withArgs(FABRIC_CONSTANTS.NetworkConfig.LEDGER_QUERY_ROLE).returns(false); + peerArray = [mockPeer1, mockPeer2, mockPeer3, mockPeer4, mockPeer5]; + mockChannel.getPeers.returns(peerArray); + }); + + it('should initialize the channel using the first peer', async () => { + mockChannel.initialize.resolves(); + await channel._initializeInternalChannel(); + sinon.assert.calledOnce(mockChannel.initialize); + }); + + it('should try other peers if initialization fails', async () => { + channel.initialized = false; + // create a real mock + mockChannel.initialize.onCall(0).rejects(new Error('connect failed')); + mockChannel.initialize.onCall(1).resolves(); + await channel._initializeInternalChannel(); + sinon.assert.calledTwice(mockChannel.initialize); + sinon.assert.calledWith(mockChannel.initialize.firstCall, {target: mockPeer1}); + sinon.assert.calledWith(mockChannel.initialize.secondCall, {target: mockPeer3}); + }); + + it('should fail if all peers fail', async () => { + channel.initialized = false; + mockChannel.initialize.onCall(0).rejects(new Error('connect failed')); + mockChannel.initialize.onCall(1).rejects(new Error('connect failed next')); + mockChannel.initialize.onCall(2).rejects(new Error('connect failed again')); + let error; + try { + await channel._initializeInternalChannel(); + } catch(_error) { + error = _error; + } + error.should.match(/connect failed again/); + sinon.assert.calledThrice(mockChannel.initialize); + sinon.assert.calledWith(mockChannel.initialize.firstCall, {target: mockPeer1}); + sinon.assert.calledWith(mockChannel.initialize.secondCall, {target: mockPeer3}); + sinon.assert.calledWith(mockChannel.initialize.thirdCall, {target: mockPeer4}); + }); + + it('should fail if there are no LEDGER_QUERY_ROLE peers', async () => { + channel.initialized = false; + mockPeer1.isInRole.withArgs(FABRIC_CONSTANTS.NetworkConfig.LEDGER_QUERY_ROLE).returns(false); + mockPeer2.isInRole.withArgs(FABRIC_CONSTANTS.NetworkConfig.LEDGER_QUERY_ROLE).returns(false); + mockPeer3.isInRole.withArgs(FABRIC_CONSTANTS.NetworkConfig.LEDGER_QUERY_ROLE).returns(false); + mockPeer4.isInRole.withArgs(FABRIC_CONSTANTS.NetworkConfig.LEDGER_QUERY_ROLE).returns(false); + mockPeer5.isInRole.withArgs(FABRIC_CONSTANTS.NetworkConfig.LEDGER_QUERY_ROLE).returns(false); + peerArray = [mockPeer1, mockPeer2, mockPeer3, mockPeer4, mockPeer5]; + mockChannel.getPeers.returns(peerArray); + return channel._initializeInternalChannel() + .should.be.rejectedWith(/no suitable peers available to initialize from/); + }); + }); + + describe('#initialize', () => { + it('should return with no action if already initialized', () => { + channel.initialized = true; + channel._initialize(); + }); + + it('should initialize the internal channels', async () => { + channel.initialized = false; + sandbox.stub(channel, '_initializeInternalChannel').returns(); + sandbox.stub(channel, '_mapPeersToMSPid').returns({}); + await channel._initialize(); + channel.initialized.should.equal(true); + }); + }); + + describe('#_mapPeersToMSPid', () => { + let peerArray; + let mockPeer4, mockPeer5; + beforeEach(() => { + mockPeer4 = sinon.createStubInstance(Peer); + mockPeer4.index = 4; + mockPeer5 = sinon.createStubInstance(Peer); + mockPeer5.index = 5; + + mockPeer1.getMspid.returns('MSP01'); + mockPeer2.getMspid.returns('MSP02'); + mockPeer3.getMspid.returns('MSP03'); + mockPeer4.getMspid.returns('MSP03'); // duplicate id + mockPeer5.getMspid.returns(); + peerArray = [mockPeer1, mockPeer2, mockPeer3, mockPeer4, mockPeer5]; + mockChannel.getPeers.returns(peerArray); + }); + + it('should initialize the peer map', async () => { + const peermap = channel._mapPeersToMSPid(); + peermap.size.should.equal(3); + peermap.get('MSP01').should.deep.equal([mockPeer1]); + peermap.get('MSP02').should.deep.equal([mockPeer2]); + peermap.get('MSP03').should.deep.equal([mockPeer3, mockPeer4]); + }); + + it('should throw error if no peers associated with MSPID', async () => { + mockChannel.getPeers.returns([]); + (() => { + channel._mapPeersToMSPid(); + }).should.throw(/no suitable peers associated with mspIds were found/); + }); + }); + + describe('#getInternalChannel', () => { + it('should return the fabric-client channel object', () => { + channel.getInternalChannel().should.equal(mockChannel); + }); + }); + + describe('#getPeerMap', () => { + it('should return the peer map', () => { + const map = new Map(); + channel.peerMap = map; + channel.getPeerMap().should.equal(map); + }); + }); + + describe('#getContract', () => { + it('should return a cached contract object', () => { + const mockContract = sinon.createStubInstance(Contract); + channel.contracts.set('foo', mockContract); + channel.getContract('foo').should.equal(mockContract); + }); + + it('should create a non-existent contract object', () => { + const contract = channel.getContract('bar'); + contract.should.be.instanceof(Contract); + contract.chaincodeId.should.equal('bar'); + }); + }); + + describe('#_dispose', () => { + it('should cleanup the channel object', () => { + const mockContract = sinon.createStubInstance(Contract); + channel.contracts.set('foo', mockContract); + channel.contracts.size.should.equal(1); + channel.initialized = true; + channel._dispose(); + channel.contracts.size.should.equal(0); + channel.initialized.should.equal(false); + }); + }); + +}); \ No newline at end of file diff --git a/fabric-network/test/contract.js b/fabric-network/test/contract.js new file mode 100644 index 0000000000..556f489954 --- /dev/null +++ b/fabric-network/test/contract.js @@ -0,0 +1,343 @@ +/** + * Copyright 2018 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; +const sinon = require('sinon'); + +const Channel = require('fabric-client/lib/Channel'); +const Peer = require('fabric-client/lib/Peer'); +const Client = require('fabric-client'); +const TransactionID = require('fabric-client/lib/TransactionID.js'); +const User = require('fabric-client/lib/User.js'); + +const chai = require('chai'); +const should = chai.should(); +chai.use(require('chai-as-promised')); + +const Contract = require('../lib/contract'); +const Network = require('../lib/network'); + + +describe('Contract', () => { + + const sandbox = sinon.createSandbox(); + let clock; + + let mockChannel, mockClient, mockUser, mockNetwork; + let mockPeer1, mockPeer2, mockPeer3; + let contract; + let mockTransactionID; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + mockChannel = sinon.createStubInstance(Channel); + mockClient = sinon.createStubInstance(Client); + mockNetwork = sinon.createStubInstance(Network); + mockNetwork.getClient.returns(mockClient); + mockUser = sinon.createStubInstance(User); + mockTransactionID = sinon.createStubInstance(TransactionID); + mockTransactionID.getTransactionID.returns('00000000-0000-0000-0000-000000000000'); + mockClient.newTransactionID.returns(mockTransactionID); + mockChannel.getName.returns('testchainid'); + + mockPeer1 = sinon.createStubInstance(Peer); + mockPeer1.index = 1; // add these so that the mockPeers can be distiguished when used in WithArgs(). + mockPeer1.getName.returns('Peer1'); + + mockPeer2 = sinon.createStubInstance(Peer); + mockPeer2.index = 2; + mockPeer2.getName.returns('Peer2'); + + mockPeer3 = sinon.createStubInstance(Peer); + mockPeer3.index = 3; + mockPeer3.getName.returns('Peer3'); + + contract = new Contract(mockChannel, 'someid', mockNetwork); + + }); + + afterEach(() => { + sandbox.restore(); + clock.restore(); + }); + + describe('#_validatePeerResponses', () => { + it('should return all responses because all are valid', () => { + const responses = [ + { + response: { + status: 200, + payload: 'no error' + } + }, + + { + response: { + status: 200, + payload: 'good here' + } + } + ]; + + mockChannel.verifyProposalResponse.returns(true); + mockChannel.compareProposalResponseResults.returns(true); + + (function() { + const {validResponses} = contract._validatePeerResponses(responses); + validResponses.should.deep.equal(responses); + }).should.not.throw(); + }); + + it('should throw if no responses', () => { + (function() { + contract._validatePeerResponses([]); + }).should.throw(/No results were returned/); + }); + + it('should throw if no proposal responses', () => { + (function() { + contract._validatePeerResponses([]); + }).should.throw(/No results were returned/); + }); + + it('should throw if all responses are either not 200 or errors', () => { + const responses = [ + { + response: { + status: 500, + payload: 'got an error' + } + }, + new Error('had a problem'), + { + response: { + status: 500, + payload: 'oh oh another error' + } + } + ]; + + mockChannel.verifyProposalResponse.returns(true); + mockChannel.compareProposalResponseResults.returns(true); + + (function() { + contract._validatePeerResponses(responses); + }).should.throw(/No valid responses/); + }); + + it('should return only the valid responses', () => { + const resp1 = { + response: { + status: 200, + payload: 'no error' + } + }; + + const resp2 = new Error('had a problem'); + + const resp3 = { + response: { + status: 500, + payload: 'such error' + } + }; + + const responses = [resp1, resp2, resp3]; + + mockChannel.verifyProposalResponse.returns(true); + mockChannel.compareProposalResponseResults.returns(true); + + (function() { + const {validResponses} = contract._validatePeerResponses(responses); + validResponses.should.deep.equal([resp1]); + + }).should.not.throw(); + + }); + + it('should log warning if verifyProposal returns false', () => { + const response1 = { + response: { + status: 200, + payload: 'NOTVALID' + } + }; + const response2 = { + response: { + status: 200, + payload: 'I AM VALID' + } + }; + + const responses = [ response1, response2 ]; + + mockChannel.verifyProposalResponse.withArgs(response1).returns(false); + mockChannel.verifyProposalResponse.withArgs(response2).returns(true); + mockChannel.compareProposalResponseResults.returns(true); + (function() { + const {validResponses, invalidResponses, invalidResponseMsgs} = contract._validatePeerResponses(responses); + validResponses.should.deep.equal([response2]); + invalidResponses.should.deep.equal([response1]); + invalidResponseMsgs.length.should.equal(1); + invalidResponseMsgs[0].should.equal('Proposal response from peer failed verification: {"status":200,"payload":"NOTVALID"}'); + }).should.not.throw(); + }); + }); + + describe('#submitTransaction', () => { + const validResponses = [{ + response: { + status: 200 + } + }]; + + + beforeEach(() => { + sandbox.stub(contract, '_validatePeerResponses').returns({validResponses: validResponses}); + }); + + it('should throw if functionName not specified', () => { + return contract.submitTransaction(null, 'arg1', 'arg2') + .should.be.rejectedWith('transactionName must be a non-empty string: null'); + }); + + + it('should throw if args contains non-string values', () => { + return contract.submitTransaction('myfunc', 'arg1', 3.142) + .should.be.rejectedWith('transaction parameters must be strings: 3.142'); + }); + + it('should submit an invoke request to the chaincode which does not return data', () => { + const proposalResponses = [{ + response: { + status: 200 + } + }]; + const proposal = { proposal: 'i do' }; + const header = { header: 'gooooal' }; + mockChannel.sendTransactionProposal.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 }).resolves(response); + return contract.submitTransaction('myfunc', 'arg1', 'arg2') + .then((result) => { + should.equal(result, null); + sinon.assert.calledOnce(mockChannel.sendTransactionProposal); + sinon.assert.calledWith(mockChannel.sendTransactionProposal, { + chaincodeId: 'someid', + txId: mockTransactionID, + fcn: 'myfunc', + args: ['arg1', 'arg2'] + }); + sinon.assert.calledOnce(mockChannel.sendTransaction); + }); + }); + + it('should submit an invoke request to the chaincode which does return data', () => { + const proposalResponses = [{ + response: { + status: 200, + payload: 'hello world' + } + }]; + const proposal = { proposal: 'i do' }; + const header = { header: 'gooooal' }; + mockChannel.sendTransactionProposal.resolves([ proposalResponses, proposal, header ]); + contract._validatePeerResponses.returns({validResponses: proposalResponses}); + // This is the commit proposal and response (from the orderer). + const response = { + status: 'SUCCESS' + }; + mockChannel.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal }).resolves(response); + return contract.submitTransaction('myfunc', 'arg1', 'arg2') + .then((result) => { + result.should.equal('hello world'); + sinon.assert.calledOnce(mockChannel.sendTransactionProposal); + sinon.assert.calledWith(mockChannel.sendTransactionProposal, { + chaincodeId: 'someid', + txId: mockTransactionID, + fcn: 'myfunc', + args: ['arg1', 'arg2'] + }); + sinon.assert.calledOnce(mockChannel.sendTransaction); + }); + }); + + it('should submit an invoke request to the chaincode', () => { + const proposalResponses = [{ + response: { + status: 200 + } + }]; + const proposal = { proposal: 'i do' }; + const header = { header: 'gooooal' }; + mockChannel.sendTransactionProposal.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 }).resolves(response); + return contract.submitTransaction('myfunc', 'arg1', 'arg2') + .then((result) => { + should.equal(result, null); + sinon.assert.calledOnce(mockClient.newTransactionID); + sinon.assert.calledOnce(mockChannel.sendTransactionProposal); + sinon.assert.calledWith(mockChannel.sendTransactionProposal, { + chaincodeId: 'someid', + txId: mockTransactionID, + fcn: 'myfunc', + args: ['arg1', 'arg2'] + }); + sinon.assert.calledOnce(mockChannel.sendTransaction); + }); + }); + + + it('should throw if transaction proposals were not valid', () => { + const proposalResponses = []; + const proposal = { proposal: 'i do' }; + const header = { header: 'gooooal' }; + const errorResp = new Error('an error'); + mockChannel.sendTransactionProposal.resolves([ proposalResponses, proposal, header ]); + contract._validatePeerResponses.withArgs(proposalResponses).throws(errorResp); + return contract.submitTransaction('myfunc', 'arg1', 'arg2') + .should.be.rejectedWith(/an error/); + }); + + it('should throw if no valid proposal responses', () => { + const proposalResponses = []; + const proposal = { proposal: 'i do' }; + const header = { header: 'gooooal' }; + //const errorResp = new Error('an error'); + mockChannel.sendTransactionProposal.resolves([ proposalResponses, proposal, header ]); + contract._validatePeerResponses.withArgs(proposalResponses).returns({ validResponses: [] }); + return contract.submitTransaction('myfunc', 'arg1', 'arg2') + .should.be.rejectedWith(/No valid responses from any peers/); + }); + + it('should throw an error if the orderer responds with an error', () => { + const proposalResponses = [{ + response: { + status: 200 + } + }]; + const proposal = { proposal: 'i do' }; + const header = { header: 'gooooal' }; + mockChannel.sendTransactionProposal.resolves([ proposalResponses, proposal, header ]); + // This is the commit proposal and response (from the orderer). + const response = { + status: 'FAILURE' + }; + mockChannel.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal }).resolves(response); + return contract.submitTransaction('myfunc', 'arg1', 'arg2') + .should.be.rejectedWith(/Failed to send/); + }); + + }); + +}); \ No newline at end of file diff --git a/fabric-network/test/impl/wallet/basewallet.js b/fabric-network/test/impl/wallet/basewallet.js new file mode 100644 index 0000000000..4daacb0838 --- /dev/null +++ b/fabric-network/test/impl/wallet/basewallet.js @@ -0,0 +1,65 @@ +/** + * Copyright 2018 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; +const chai = require('chai'); +chai.use(require('chai-as-promised')); +const should = chai.should(); + +const BaseWallet = require('../../../lib/impl/wallet/basewallet'); +const X509WalletMixin = require('../../../lib/impl/wallet/x509walletmixin'); + + +describe('BaseWallet', () => { + describe('#constructor', () => { + it('should default to X509 wallet mixin', () => { + const wallet = new BaseWallet(); + wallet.walletMixin.should.be.an.instanceof(X509WalletMixin); + }); + + it('should accept a mixin parameter', () => { + const wallet = new BaseWallet('my_mixin'); + wallet.walletMixin.should.equal('my_mixin'); + }); + }); + + describe('#setUserContext', () => { + const wallet = new BaseWallet(); + + + }); + + describe('Unimplemented methods', () => { + const wallet = new BaseWallet(); + + it('throws exception calling import()', () => { + return wallet.import(null, null).should.be.rejectedWith('Not implemented'); + }); + + it('throws exception calling export()', () => { + return wallet.export(null).should.be.rejectedWith('Not implemented'); + }); + + it('throws exception calling delete()', () => { + return wallet.delete(null).should.be.rejectedWith('Not implemented'); + }); + + it('throws exception calling exists()', () => { + return wallet.exists(null).should.be.rejectedWith('Not implemented'); + }); + + it('throws exception calling getCryptoSuite()', () => { + return wallet.getCryptoSuite(null).should.be.rejectedWith('Not implemented'); + }); + + it('throws exception calling getAllLabels()', async () => { + const labels = await wallet.getAllLabels(); + should.equal(labels, null); + }); + + }); + +}); \ No newline at end of file diff --git a/fabric-network/test/impl/wallet/inmemorywallet.js b/fabric-network/test/impl/wallet/inmemorywallet.js new file mode 100644 index 0000000000..5a4bc436ed --- /dev/null +++ b/fabric-network/test/impl/wallet/inmemorywallet.js @@ -0,0 +1,220 @@ +/** + * Copyright 2018 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; +const sinon = require('sinon'); +const chai = require('chai'); +chai.use(require('chai-as-promised')); +const should = chai.should(); + +const InMemoryWallet = require('../../../lib/impl/wallet/inmemorywallet'); +const X509WalletMixin = require('../../../lib/impl/wallet/x509walletmixin'); +const Client = require('fabric-client'); +const api = require('fabric-client/lib/api.js'); + + +describe('InMemoryWallet', () => { + describe('#constructor', () => { + it('should default to X509 wallet mixin', () => { + const wallet = new InMemoryWallet(); + wallet.walletMixin.should.be.an.instanceof(X509WalletMixin); + }); + + it('should accept a mixin parameter', () => { + const wallet = new InMemoryWallet('my_mixin'); + wallet.walletMixin.should.equal('my_mixin'); + }); + }); + + describe('#getStateStore', () => { + const wallet = new InMemoryWallet(); + + it('should create a KV store', async () => { + const store = await wallet.getStateStore('test'); + store.should.be.an.instanceof(api.KeyValueStore); + }); + }); + + describe('#getCryptoSuite', () => { + const wallet = new InMemoryWallet(); + + it('should create a KV store', async () => { + const suite = await wallet.getCryptoSuite('test'); + suite.should.be.an.instanceof(api.CryptoSuite); + }); + }); + + describe('#setUserContext', () => { + const sandbox = sinon.createSandbox(); + let wallet; + let mockClient; + + beforeEach(() => { + wallet = new InMemoryWallet(); + mockClient = sinon.createStubInstance(Client); + }); + + afterEach(() => { + sandbox.restore(); + }); + + + it('should throw setting the user context for an unregistered id', async () => { + return wallet.setUserContext(new Client(), 'test').should.be.rejectedWith('identity \'test\' isn\'t enrolled, or loaded'); + }); + + it('should return loaded identity', async () => { + const mockId = { + isEnrolled: () => true + }; + mockClient.getUserContext.withArgs('test', true).returns(mockId); + const id = await wallet.setUserContext(mockClient, 'test'); + should.equal(id, mockId); + }); + + describe('#configureClientStores', () => { + it('should set the crypto suite', async () => { + wallet.walletMixin.getCryptoSuite = (label) => { + return wallet.getCryptoSuite(label); + }; + const client = await wallet.configureClientStores(mockClient, 'test'); + mockClient.should.equal(client); + }); + }); + + }); + + describe('label storage', () => { + let wallet; + const cert = `-----BEGIN CERTIFICATE----- +MIICfzCCAiWgAwIBAgIUNAqZVk9s5/HR7k30feNp8DrYbK4wCgYIKoZIzj0EAwIw +cDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh +biBGcmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xGTAXBgNVBAMT +EG9yZzEuZXhhbXBsZS5jb20wHhcNMTgwMjI2MjAwOTAwWhcNMTkwMjI2MjAxNDAw +WjBdMQswCQYDVQQGEwJVUzEXMBUGA1UECBMOTm9ydGggQ2Fyb2xpbmExFDASBgNV +BAoTC0h5cGVybGVkZ2VyMQ8wDQYDVQQLEwZjbGllbnQxDjAMBgNVBAMTBWFkbWlu +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEz05miTKv6Vz+qhc5362WIZ44fs/H +X5m9zDOifle5HIjt4Usj+TiUgT1hpbI8UI9pueWhbrZpZXlX6+mImi52HaOBrzCB +rDAOBgNVHQ8BAf8EBAMCA6gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC +MAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPnxMtT6jgYsMAgI38ponGs8sgbqMCsG +A1UdIwQkMCKAIKItrzVrKqtXkupT419m/M7x1/GqKzorktv7+WpEjqJqMCEGA1Ud +EQQaMBiCFnBlZXIwLm9yZzEuZXhhbXBsZS5jb20wCgYIKoZIzj0EAwIDSAAwRQIh +AM1JowZMshCRs6dnOfRmUHV7399KnNvs5QoNw93cuQuAAiBtBEGh1Xt50tZjDcYN +j+yx4IraL4JvMrCHbR5/R+Xo1Q== +-----END CERTIFICATE-----`; + const key = `-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgbTXpl4NGXuPtSC/V +PTVNGVBgVv8pZ6kGktVcnQD0KiKhRANCAATPTmaJMq/pXP6qFznfrZYhnjh+z8df +mb3MM6J+V7kciO3hSyP5OJSBPWGlsjxQj2m55aFutmlleVfr6YiaLnYd +-----END PRIVATE KEY----- +`; + const identity1 = { + certificate: cert, + privateKey: key, + mspId: 'mspOrg1' + }; + const identity2 = { + certificate: cert, + privateKey: key, + mspId: 'mspOrg2' + }; + + + beforeEach(async () => { + wallet = new InMemoryWallet(); + await wallet.import('user1', identity1); + await wallet.import('user2', identity2); + }); + + describe('#import', () => { + it('should throw if there is no wallet mixin', () => { + wallet = new InMemoryWallet(null); + return wallet.import(null, null).should.be.rejectedWith('no import method exists'); + }); + }); + + describe('#export', () => { + it('should export the wallet', async () => { + const id = await wallet.export('user1'); + identity1.mspId.should.equal(id.mspId); + identity1.certificate.should.equal(id.certificate); + }); + + it('should return null if export an identity that\'s not in the wallet', async () => { + const id = await wallet.export('user3'); + should.equal(id, null); + }); + + it('should throw if there is no wallet mixin', () => { + wallet = new InMemoryWallet(null); + return wallet.export(null, null).should.be.rejectedWith('no export method exists'); + }); + }); + + describe('#exists', () => { + it('should test the existence of an identity from the wallet', async () => { + let exists = await wallet.exists('user1'); + exists.should.equal(true); + exists = await wallet.exists('user2'); + exists.should.equal(true); + }); + + it('should test the non-existence of an identity from the wallet', async () => { + let exists = await wallet.exists('user3'); + exists.should.equal(false); + }); + }); + + describe('#delete', () => { + it('should delete an identity from the wallet', async () => { + let exists = await wallet.exists('user1'); + exists.should.equal(true); + await wallet.delete('user1'); + exists = await wallet.exists('user1'); + exists.should.equal(false); + }); + }); + + describe('#getAllLabels', () => { + it('should list all identities in the wallet', async () => { + const labels = await wallet.getAllLabels(); + // labels.length.should.equal(2); + labels.includes('user1').should.equal(true); + labels.includes('user2').should.equal(true); + }); + }); + + describe('#list', () => { + it('should list all identities in the wallet', async () => { + const list = await wallet.list(); + const labels = list.map(item => item.label); + labels.includes('user1').should.equal(true); + labels.includes('user2').should.equal(true); + }); + }); + + describe('#list', () => { + it('should return an empty list for no identities in the wallet', async () => { + let labels = await wallet.getAllLabels(); + labels.forEach(async label => await wallet.delete(label)); + const list = await wallet.list(); + list.length.should.equal(0); + }); + }); + + }); + + describe('InMemoryKVS', async () => { + const wallet = new InMemoryWallet(); + const store = await wallet.getStateStore('test'); + + it('#getValue', async () => { + await store.setValue('user1', 'val1'); + const value = await store.getValue('user1'); + const value2 = await store.getValue('user3'); + }); + }); +}); \ No newline at end of file diff --git a/fabric-network/test/impl/wallet/x509walletmixin.js b/fabric-network/test/impl/wallet/x509walletmixin.js new file mode 100644 index 0000000000..6221db970f --- /dev/null +++ b/fabric-network/test/impl/wallet/x509walletmixin.js @@ -0,0 +1,6 @@ +/** + * Copyright 2018 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + diff --git a/fabric-network/test/network.js b/fabric-network/test/network.js new file mode 100644 index 0000000000..b24fbb2a31 --- /dev/null +++ b/fabric-network/test/network.js @@ -0,0 +1,290 @@ +/** + * Copyright 2018 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; +const sinon = require('sinon'); +const rewire = require('rewire'); + +const InternalChannel = rewire('fabric-client/lib/Channel'); +const Peer = InternalChannel.__get__('ChannelPeer'); +const FABRIC_CONSTANTS = require('fabric-client/lib/Constants'); + +const Client = require('fabric-client'); + +const chai = require('chai'); +const should = chai.should(); +chai.use(require('chai-as-promised')); + +const Channel = require('../lib/channel'); +const Network = require('../lib/network'); +const Wallet = require('../lib/api/wallet'); + + +describe('Network', () => { + + const sandbox = sinon.createSandbox(); + let clock; + + let mockClient; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + mockClient = sinon.createStubInstance(Client); + + }); + + afterEach(() => { + sandbox.restore(); + clock.restore(); + }); + + + describe('#_mergeOptions', () => { + let defaultOptions; + + beforeEach(() => { + defaultOptions = { + commitTimeout: 300 * 1000, + }; + }); + + it('should return the default options when there are no overrides', () => { + const overrideOptions = {}; + const expectedOptions = { + commitTimeout: 300 * 1000 + }; + Network._mergeOptions(defaultOptions, overrideOptions); + defaultOptions.should.deep.equal(expectedOptions); + }); + + it('should change a default option', () => { + const overrideOptions = { + commitTimeout: 1234 + }; + const expectedOptions = { + commitTimeout: 1234 + }; + Network._mergeOptions(defaultOptions, overrideOptions); + defaultOptions.should.deep.equal(expectedOptions); + }); + + it('should add a new option', () => { + const overrideOptions = { + useDiscovery: true + }; + const expectedOptions = { + commitTimeout: 300 * 1000, + useDiscovery: true + }; + Network._mergeOptions(defaultOptions, overrideOptions); + defaultOptions.should.deep.equal(expectedOptions); + }); + + it('should add option structures', () => { + const overrideOptions = { + identity: 'admin', + useDiscovery: true, + discoveryOptions: { + discoveryProtocol: 'grpc', + asLocalhost: true + } + }; + const expectedOptions = { + commitTimeout: 300 * 1000, + identity: 'admin', + useDiscovery: true, + discoveryOptions: { + discoveryProtocol: 'grpc', + asLocalhost: true + } + }; + Network._mergeOptions(defaultOptions, overrideOptions); + defaultOptions.should.deep.equal(expectedOptions); + }); + + it('should merge option structures', () => { + defaultOptions = { + commitTimeout: 300 * 1000, + identity: 'user', + useDiscovery: true, + discoveryOptions: { + discoveryProtocol: 'grpc', + asLocalhost: false + } + }; + const overrideOptions = { + identity: 'admin', + useDiscovery: true, + discoveryOptions: { + asLocalhost: true + } + }; + const expectedOptions = { + commitTimeout: 300 * 1000, + identity: 'admin', + useDiscovery: true, + discoveryOptions: { + discoveryProtocol: 'grpc', + asLocalhost: true + } + }; + Network._mergeOptions(defaultOptions, overrideOptions); + defaultOptions.should.deep.equal(expectedOptions); + }); + + }); + + describe('#constructor', () => { + it('should instantiate a Network object', () => { + const network = new Network(); + network.channels.should.be.instanceof(Map); + network.options.should.deep.equal({ commitTimeout: 300 * 1000 }); + }); + }); + + describe('#initialize', () => { + let network; + let mockWallet; + + beforeEach(() => { + network = new Network(); + mockWallet = sinon.createStubInstance(Wallet); + sandbox.stub(Client, 'loadFromConfig').withArgs('ccp').returns(mockClient); + mockWallet.setUserContext.withArgs(mockClient, 'admin').returns('foo'); + }); + + it('should fail without options supplied', () => { + return network.initialize() + .should.be.rejectedWith(/A wallet must be assigned to a Network instance/); + }); + + it('should fail without wallet option supplied', () => { + const options = { + identity: 'admin' + }; + return network.initialize('ccp', options) + .should.be.rejectedWith(/A wallet must be assigned to a Network instance/); + }); + + it('should initialize the network', async () => { + const options = { + wallet: mockWallet, + }; + await network.initialize('ccp', options); + network.client.should.equal(mockClient); + should.not.exist(network.currentIdentity); + }); + + it('should initialize the network with identity', async () => { + const options = { + wallet: mockWallet, + identity: 'admin' + }; + await network.initialize('ccp', options); + network.client.should.equal(mockClient); + network.currentIdentity.should.equal('foo'); + }); + + it('should initialize from an existing client object', async () => { + const options = { + wallet: mockWallet, + identity: 'admin' + }; + await network.initialize(mockClient, options); + network.client.should.equal(mockClient); + network.currentIdentity.should.equal('foo'); + }); + + }); + + describe('getters', () => { + let network; + let mockWallet; + + beforeEach(async () => { + network = new Network(); + mockWallet = sinon.createStubInstance(Wallet); + sandbox.stub(Client, 'loadFromConfig').withArgs('ccp').returns(mockClient); + mockWallet.setUserContext.withArgs(mockClient, 'admin').returns('foo'); + const options = { + wallet: mockWallet, + identity: 'admin' + }; + await network.initialize('ccp', options); + + }); + + describe('#getCurrentIdentity', () => { + it('should return the initialized identity', () => { + network.getCurrentIdentity().should.equal('foo'); + }); + }); + + describe('#getClient', () => { + it('should return the underlying client object', () => { + network.getClient().should.equal(mockClient); + }); + }); + + describe('#getOptions', () => { + it('should return the initialized options', () => { + const expectedOptions = { + commitTimeout: 300 * 1000, + wallet: mockWallet, + identity: 'admin' + }; + network.getOptions().should.deep.equal(expectedOptions); + }); + }); + }); + + describe('channel interactions', () => { + let network; + let mockChannel; + let mockInternalChannel; + + beforeEach(() => { + network = new Network(); + mockChannel = sinon.createStubInstance(Channel); + network.channels.set('foo', mockChannel); + network.client = mockClient; + + mockInternalChannel = sinon.createStubInstance(InternalChannel); + const mockPeer1 = sinon.createStubInstance(Peer); + mockPeer1.index = 1; // add these so that the mockPeers can be distiguished when used in WithArgs(). + mockPeer1.getName.returns('Peer1'); + mockPeer1.getMspid.returns('MSP01'); + mockPeer1.isInRole.withArgs(FABRIC_CONSTANTS.NetworkConfig.LEDGER_QUERY_ROLE).returns(true); + const peerArray = [mockPeer1]; + mockInternalChannel.getPeers.returns(peerArray); + }); + + describe('#getChannel', () => { + it('should return a cached channel object', () => { + network.getChannel('foo').should.eventually.equal(mockChannel); + }); + + it('should create a non-existent channel object', async () => { + mockClient.getChannel.withArgs('bar').returns(mockInternalChannel); + + const channel2 = await network.getChannel('bar'); + channel2.should.be.instanceof(Channel); + channel2.network.should.equal(network); + channel2.channel.should.equal(mockInternalChannel); + network.channels.size.should.equal(2); + }); + }); + + describe('#dispose', () => { + it('should cleanup the network and its channels', () => { + network.channels.size.should.equal(1); + network.dispose(); + network.channels.size.should.equal(0); + }); + }); + }); + +}); \ No newline at end of file diff --git a/package.json b/package.json index 899dc0d11b..99eb221fa1 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ }, "nyc": { "include": [ + "fabric-network/lib/**/*.js", "fabric-client/lib/**/*.js", "fabric-ca-client/lib/FabricCAClientImpl.js", "fabric-ca-client/lib/helper.js", diff --git a/test/integration/network-e2e/e2e.js b/test/integration/network-e2e/e2e.js new file mode 100644 index 0000000000..04ffd6f120 --- /dev/null +++ b/test/integration/network-e2e/e2e.js @@ -0,0 +1,10 @@ +/** + * Copyright 2017 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ +require('../e2e/create-channel.js'); +require('../e2e/join-channel.js'); +require('./install-chaincode.js'); +require('./instantiate-chaincode.js'); +require('./invoke.js'); diff --git a/test/integration/network-e2e/install-chaincode.js b/test/integration/network-e2e/install-chaincode.js new file mode 100644 index 0000000000..a8ef2e6923 --- /dev/null +++ b/test/integration/network-e2e/install-chaincode.js @@ -0,0 +1,42 @@ +/** + * Copyright 2017 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +// This is an end-to-end test that focuses on exercising all parts of the fabric APIs +// in a happy-path scenario +'use strict'; + +var utils = require('fabric-client/lib/utils.js'); +var logger = utils.getLogger('E2E install-chaincode'); + +var tape = require('tape'); +var _test = require('tape-promise').default; +var test = _test(tape); + +var e2eUtils = require('../e2e/e2eUtils.js'); +var testUtil = require('../../unit/util.js'); +var version = 'v0'; + +test('\n\n***** Network End-to-end flow: chaincode install *****\n\n', (t) => { + e2eUtils.installChaincodeWithId('org1', testUtil.NETWORK_END2END.chaincodeId, testUtil.NODE_CHAINCODE_PATH, testUtil.METADATA_PATH, version, 'node', t, true) + .then(() => { + t.pass('Successfully installed chaincode in peers of organization "org1"'); + return e2eUtils.installChaincodeWithId('org2', testUtil.NETWORK_END2END.chaincodeId, testUtil.NODE_CHAINCODE_PATH, testUtil.METADATA_PATH, version, 'node', t, true); + }, (err) => { + t.fail('Failed to install chaincode in peers of organization "org1". ' + err.stack ? err.stack : err); + logger.error('Failed to install chaincode in peers of organization "org1". '); + return e2eUtils.installChaincodeWithId('org2', testUtil.NETWORK_END2END.chaincodeId, testUtil.NODE_CHAINCODE_PATH, testUtil.METADATA_PATH, version, 'node', t, true); + }).then(() => { + t.pass('Successfully installed chaincode in peers of organization "org2"'); + t.end(); + }, (err) => { + t.fail('Failed to install chaincode in peers of organization "org2". ' + err.stack ? err.stack : err); + logger.error('Failed to install chaincode in peers of organization "org2". '); + t.end(); + }).catch((err) => { + t.fail('Test failed due to unexpected reasons. ' + err.stack ? err.stack : err); + t.end(); + }); +}); diff --git a/test/integration/network-e2e/instantiate-chaincode.js b/test/integration/network-e2e/instantiate-chaincode.js new file mode 100644 index 0000000000..a28dd15ff4 --- /dev/null +++ b/test/integration/network-e2e/instantiate-chaincode.js @@ -0,0 +1,46 @@ +/** + * Copyright 2017 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +// This is an end-to-end test that focuses on exercising all parts of the fabric APIs +// in a happy-path scenario +'use strict'; + +var utils = require('fabric-client/lib/utils.js'); +var logger = utils.getLogger('E2E instantiate-chaincode'); +logger.level = 'debug'; + +var tape = require('tape'); +var _test = require('tape-promise').default; +var test = _test(tape); + +var e2eUtils = require('../e2e/e2eUtils.js'); +var testUtil = require('../../unit/util.js'); + + + +test('\n\n***** Network End-to-end flow: instantiate chaincode *****\n\n', (t) => { + e2eUtils.instantiateChaincodeWithId('org1', testUtil.NETWORK_END2END.chaincodeId, testUtil.NODE_CHAINCODE_PATH, 'v0', 'node', false, false, t) + .then((result) => { + if(result){ + t.pass('Successfully instantiated chaincode on the channel'); + + return testUtil.sleep(5000); + } + else { + t.fail('Failed to instantiate chaincode '); + t.end(); + } + }, (err) => { + t.fail('Failed to instantiate chaincode on the channel. ' + err.stack ? err.stack : err); + t.end(); + }).then(() => { + logger.debug('Successfully slept 5s to wait for chaincode instantiate to be completed and committed in all peers'); + t.end(); + }).catch((err) => { + t.fail('Test failed due to unexpected reasons. ' + err.stack ? err.stack : err); + t.end(); + }); +}); diff --git a/test/integration/network-e2e/invoke.js b/test/integration/network-e2e/invoke.js new file mode 100644 index 0000000000..244326e106 --- /dev/null +++ b/test/integration/network-e2e/invoke.js @@ -0,0 +1,84 @@ +/** + * Copyright 2018 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +// This is an end-to-end test that focuses on exercising all parts of the fabric APIs +// in a happy-path scenario +'use strict'; + +const tape = require('tape'); +const _test = require('tape-promise').default; +const test = _test(tape); +const {Network, InMemoryWallet, X509WalletMixin} = require('../../../fabric-network/index.js'); +const fs = require('fs'); + +const e2eUtils = require('../e2e/e2eUtils.js'); +const testUtils = require('../../unit/util'); +const channelName = testUtils.NETWORK_END2END.channel; +const chaincodeId = testUtils.NETWORK_END2END.chaincodeId; + +test('\n\n***** Network End-to-end flow: invoke transaction to move money *****\n\n', async (t) => { + try { + const fixtures = process.cwd() + '/test/fixtures'; + const credPath = fixtures + '/channel/crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls'; + const cert = fs.readFileSync(credPath + '/cert.pem').toString(); + const key = fs.readFileSync(credPath + '/key.pem').toString(); + const inMemoryWallet = new InMemoryWallet(); + await inMemoryWallet.import('admin', X509WalletMixin.createIdentity('Org1MSP', cert, key)); + const exists = await inMemoryWallet.exists('admin'); + + if(exists) { + t.pass('Successfully imported admin into wallet'); + } else { + t.fail('Failed to import admin into wallet'); + } + + const network = new Network(); + + const ccp = fs.readFileSync(fixtures + '/network.json'); + await network.initialize(JSON.parse(ccp.toString()), { + wallet: inMemoryWallet, + identity: 'admin' + }); + + const tlsInfo = await e2eUtils.tlsEnroll('org1'); + network.getClient().setTlsClientCertAndKey(tlsInfo.certificate, tlsInfo.key); + + t.pass('Initialized the network'); + + const channel = await network.getChannel(channelName); + + t.pass('Initialized the channel, ' + channelName); + + const contract = await channel.getContract(chaincodeId); + + t.pass('Got the contract, about to submit "move" transaction'); + + let response = await contract.submitTransaction('move', 'a', 'b','100'); + + const expectedResult = 'move succeed'; + if(response.toString() === expectedResult){ + t.pass('Successfully invoked transaction chaincode on channel'); + } + else { + t.fail('Unexpected response from transaction chaincode: ' + response); + } + + try { + response = await contract.submitTransaction('throwError', 'a', 'b','100'); + t.fail('Transaction "throwError" should have thrown an error. Got response: ' + response.toString()); + } catch(expectedErr) { + if(expectedErr.message.includes('throwError: an error occurred')) { + t.pass('Successfully handled invocation errors'); + } else { + t.fail('Unexpected exception: ' + expectedErr.message); + } + } + } catch(err) { + t.fail('Failed to invoke transaction chaincode on channel. ' + err.stack ? err.stack : err); + } + + t.end(); +}); diff --git a/test/integration/query.js b/test/integration/query.js index 81babbcd29..fe4f91ee0e 100644 --- a/test/integration/query.js +++ b/test/integration/query.js @@ -158,7 +158,7 @@ test(' ---->>>>> Query channel working <<<<<-----', (t) => { .payload.action.proposal_response_payload.extension.results.ns_rwset['0'] .rwset.writes['0'].key, 'test for write set key value'); - t.equals('6', processed_transaction.transactionEnvelope.payload.data.actions['0'] + t.equals('8', processed_transaction.transactionEnvelope.payload.data.actions['0'] .payload.action.proposal_response_payload.extension.results.ns_rwset['0'] .rwset.reads[1].version.block_num.toString(), 'test for read set block num'); diff --git a/test/unit/util.js b/test/unit/util.js index d9e6c19cca..f9a0c7842f 100644 --- a/test/unit/util.js +++ b/test/unit/util.js @@ -42,6 +42,13 @@ module.exports.NODE_END2END = { chaincodeVersion: 'v0' }; +module.exports.NETWORK_END2END = { + channel: 'mychannel', + chaincodeId: 'network-e2enodecc', + chaincodeLanguage: 'node', + chaincodeVersion: 'v0' +}; + // all temporary files and directories are created under here const tempdir = Constants.tempdir;