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;