From dbc864bdcadbe93dc7816d80833b432b7c4699b1 Mon Sep 17 00:00:00 2001 From: Attila Klenik Date: Mon, 9 Sep 2019 09:44:21 +0000 Subject: [PATCH] Add declarative validation for Fabric network config files Signed-off-by: Attila Klenik --- packages/caliper-core/lib/config/Config.js | 107 +- .../caliper-core/lib/config/config-util.js | 58 +- packages/caliper-core/lib/config/default.yaml | 6 + .../caliper-core/lib/utils/caliper-utils.js | 18 + .../caliper-fabric/lib/configValidator.js | 587 +++ packages/caliper-fabric/lib/fabric.js | 38 +- packages/caliper-fabric/lib/fabricNetwork.js | 590 +-- packages/caliper-fabric/package.json | 17 +- .../caliper-fabric/test/configValidator.js | 3674 +++++++++++++++++ .../2org1peercouchdb/fabric-node.yaml | 4 +- 10 files changed, 4496 insertions(+), 603 deletions(-) create mode 100644 packages/caliper-fabric/lib/configValidator.js create mode 100644 packages/caliper-fabric/test/configValidator.js diff --git a/packages/caliper-core/lib/config/Config.js b/packages/caliper-core/lib/config/Config.js index c9c084ac0..e61a6ec04 100644 --- a/packages/caliper-core/lib/config/Config.js +++ b/packages/caliper-core/lib/config/Config.js @@ -20,6 +20,65 @@ const nconf = require('nconf'); nconf.formats.yaml = require('nconf-yaml'); +const keys = { + Bind: { + Sut: 'caliper-bind-sut', + Sdk: 'caliper-bind-sdk', + Args: 'caliper-bind-args', + Cwd: 'caliper-bind-cwd' + }, + Workspace: 'caliper-workspace', + ProjectConfig: 'caliper-projectconfig', + UserConfig: 'caliper-userconfig', + MachineConfig: 'caliper-machineconfig', + BenchConfig: 'caliper-benchconfig', + NetworkConfig: 'caliper-networkconfig', + ZooAddress: 'caliper-zooaddress', + ZooConfig: 'caliper-zooconfig', + TxUpdateTime: 'caliper-txupdatetime', + Logging: 'caliper-logging', + Flow: { + Skip: { + Start : 'caliper-flow-skip-start', + Init: 'caliper-flow-skip-init', + Install: 'caliper-flow-skip-install', + Test: 'caliper-flow-skip-test', + End: 'caliper-flow-skip-end' + }, + Only: { + Start: 'caliper-flow-only-start', + Init: 'caliper-flow-only-init', + Install: 'caliper-flow-only-install', + Test: 'caliper-flow-only-test', + End: 'caliper-flow-only-end' + } + }, + Fabric: { + SleepAfter: { + CreateChannel: 'caliper-fabric-sleepafter-createchannel', + JoinChannel: 'caliper-fabric-sleepafter-joinchannel', + InstantiateChaincode: 'caliper-fabric-sleepafter-instantiatechaincode', + }, + Verify: { + ProposalResponse: 'caliper-fabric-verify-proposalresponse', + ReadWriteSets: 'caliper-fabric-verify-readwritesets', + }, + Timeout: { + ChaincodeInstantiate: 'caliper-fabric-timeout-chaincodeinstantiate', + ChaincodeInstantiateEvent: 'caliper-fabric-timeout-chaincodeinstantiateevent', + InvokeOrQuery: 'caliper-fabric-timeout-invokeorquery', + }, + LoadBalancing: 'caliper-fabric-loadbalancing', + OverwriteGopath: 'caliper-fabric-overwritegopath', + LatencyThreshold: 'caliper-fabric-latencythreshold', + CountQueryAsLoad: 'caliper-fabric-countqueryasload', + SkipCreateChannelPrefix: 'caliper-fabric-skipcreatechannel-', + Gateway: 'caliper-fabric-usegateway', + GatewayLocalHost: 'caliper-fabric-gatewaylocalhost', + Discovery: 'caliper-fabric-discovery' + } +}; + /** * Normalizes the key of the given setting. * @param {{key: string, value: any}} kvPair The setting as a key-value pair. @@ -44,6 +103,25 @@ function getFileParsingOptions(filename) { return { file: filename, logicalSeparator: '-', format: nconf.formats.yaml }; } +/** + * Creates an absolute path from the provided relative path if necessary. + * @param {String} relOrAbsPath The relative or absolute path to convert to an absolute path. + * Relative paths are considered relative to the Caliper root folder. + * @param {String} root_path root path to use + * @return {String} The resolved absolute path. + */ +function resolvePath(relOrAbsPath, root_path) { + if (!relOrAbsPath) { + throw new Error('Config.resolvePath: Parameter is undefined'); + } + + if (path.isAbsolute(relOrAbsPath)) { + return relOrAbsPath; + } + + return path.join(root_path, relOrAbsPath); +} + /** * The class encapsulating the hierarchy of runtime configurations. * @type {Config} @@ -70,27 +148,31 @@ class Config { // normalize the argument names to be more robust this._config.env({ parseValues: true, transform: normalizeSettingKey }); - // TODO: resolve the paths according to the workspace, once it's set through the config API - // if "caliper-projectconfig" is set at this point, include that file - let projectConf = this.get('caliper-projectconfig', undefined); + let projectConf = this.get(keys.ProjectConfig, undefined); if (projectConf && (typeof projectConf === 'string')) { - this._config.file('project', getFileParsingOptions(projectConf)); - } else if (fs.existsSync('caliper.yaml')) { - // check whether caliper.yaml is present in the current working directory for convenience - this._config.file('project', getFileParsingOptions('caliper.yaml')); + let projectConfFile = resolvePath(projectConf, this.get(keys.Workspace, '.')); + this._config.file('project', getFileParsingOptions(projectConfFile)); + } else { + // check whether caliper.yaml is present in the workspace directory for convenience + let projectConfFile = resolvePath('caliper.yaml', this.get(keys.Workspace, '.')); + if (fs.existsSync(projectConfFile)) { + this._config.file('project', getFileParsingOptions(projectConfFile)); + } } // if "caliper-userconfig" is set at this point, include that file - let userConfig = this.get('caliper-userconfig', undefined); + let userConfig = this.get(keys.UserConfig, undefined); if (userConfig && (typeof userConfig === 'string')) { - this._config.file('user', getFileParsingOptions(userConfig)); + let userConfFile = resolvePath(userConfig, this.get(keys.Workspace, '.')); + this._config.file('user', getFileParsingOptions(userConfFile)); } // if "caliper-machineconfig" is set at this point, include that file - let machineConfig = this.get('caliper-machineconfig', undefined); - if (machineConfig) { - this._config.file('machine', getFileParsingOptions(machineConfig)); + let machineConfig = this.get(keys.MachineConfig, undefined); + if (machineConfig && (typeof machineConfig === 'string')) { + let machineConfFile = resolvePath(machineConfig, this.get(keys.Workspace, '.')); + this._config.file('machine', getFileParsingOptions(machineConfFile)); } // as fallback, always include the default config packaged with Caliper @@ -135,4 +217,5 @@ class Config { } module.exports = Config; +module.exports.keys = keys; diff --git a/packages/caliper-core/lib/config/config-util.js b/packages/caliper-core/lib/config/config-util.js index 6b476afd6..52378abbc 100644 --- a/packages/caliper-core/lib/config/config-util.js +++ b/packages/caliper-core/lib/config/config-util.js @@ -52,62 +52,6 @@ function get(name, defaultValue) { return _getConfigInstance().get(name, defaultValue); } -const keys = { - Bind: { - Sut: 'caliper-bind-sut', - Sdk: 'caliper-bind-sdk', - Args: 'caliper-bind-args', - Cwd: 'caliper-bind-cwd' - }, - Workspace: 'caliper-workspace', - BenchConfig: 'caliper-benchconfig', - NetworkConfig: 'caliper-networkconfig', - ZooAddress: 'caliper-zooaddress', - ZooConfig: 'caliper-zooconfig', - TxUpdateTime: 'caliper-txupdatetime', - Logging: 'caliper-logging', - Flow: { - Skip: { - Start : 'caliper-flow-skip-start', - Init: 'caliper-flow-skip-init', - Install: 'caliper-flow-skip-install', - Test: 'caliper-flow-skip-test', - End: 'caliper-flow-skip-end' - }, - Only: { - Start: 'caliper-flow-only-start', - Init: 'caliper-flow-only-init', - Install: 'caliper-flow-only-install', - Test: 'caliper-flow-only-test', - End: 'caliper-flow-only-end' - } - }, - Fabric: { - SleepAfter: { - CreateChannel: 'caliper-fabric-sleepafter-createchannel', - JoinChannel: 'caliper-fabric-sleepafter-joinchannel', - InstantiateChaincode: 'caliper-fabric-sleepafter-instantiatechaincode', - }, - Verify: { - ProposalResponse: 'caliper-fabric-verify-proposalresponse', - ReadWriteSets: 'caliper-fabric-verify-readwritesets', - }, - Timeout: { - ChaincodeInstantiate: 'caliper-fabric-timeout-chaincodeinstantiate', - ChaincodeInstantiateEvent: 'caliper-fabric-timeout-chaincodeinstantiateevent', - InvokeOrQuery: 'caliper-fabric-timeout-invokeorquery', - }, - LoadBalancing: 'caliper-fabric-loadbalancing', - OverwriteGopath: 'caliper-fabric-overwritegopath', - LatencyThreshold: 'caliper-fabric-latencythreshold', - CountQueryAsLoad: 'caliper-fabric-countqueryasload', - SkipCreateChannelPrefix: 'caliper-fabric-skipcreatechannel-', - Gateway: 'caliper-fabric-usegateway', - GatewayLocalHost: 'caliper-fabric-gatewaylocalhost', - Discovery: 'caliper-fabric-discovery' - } -}; - module.exports.get = get; module.exports.set = set; -module.exports.keys = keys; +module.exports.keys = Config.keys; diff --git a/packages/caliper-core/lib/config/default.yaml b/packages/caliper-core/lib/config/default.yaml index 79aa77085..b0165de7d 100644 --- a/packages/caliper-core/lib/config/default.yaml +++ b/packages/caliper-core/lib/config/default.yaml @@ -25,6 +25,12 @@ caliper: args: # Workspace directory that contains all configuration information workspace: './' + # The file path for the project-level configuration file. Can be relative to the workspace. + projectconfig: + # The file path for the user-level configuration file. Can be relative to the workspace. + userconfig: + # The file path for the user-level configuration file. Can be relative to the workspace. + machineconfig: # Path to the benchmark workload file that describes the test client(s), test rounds and monitor benchconfig: # Path to the blockchain configuration file that contains information required to interact with the SUT diff --git a/packages/caliper-core/lib/utils/caliper-utils.js b/packages/caliper-core/lib/utils/caliper-utils.js index 04746aad2..b2de1905f 100644 --- a/packages/caliper-core/lib/utils/caliper-utils.js +++ b/packages/caliper-core/lib/utils/caliper-utils.js @@ -95,6 +95,24 @@ class CaliperUtils { } } + /** + * Convert an object to YAML string. + * @param {object} obj The object to stringify. + * @return {string} The string YAML content. + */ + static stringifyYaml(obj) { + if (!obj) { + throw new Error('Util.stringifyYaml: object to stringify is undefined'); + } + + try{ + return yaml.safeDump(obj); + } + catch(err) { + throw new Error(`Failed to stringify object: ${(err.message || err)}`); + } + } + /** * Parse a YAML conform string into an object. * @param {string} stringContent The YAML content. diff --git a/packages/caliper-fabric/lib/configValidator.js b/packages/caliper-fabric/lib/configValidator.js new file mode 100644 index 000000000..8f86d8fb8 --- /dev/null +++ b/packages/caliper-fabric/lib/configValidator.js @@ -0,0 +1,587 @@ +/* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +const j = require('@hapi/joi'); + +/** + * Utility class for the declarative validation of Fabric network configuration objects. + */ +class ConfigValidator { + /** + * Validates the entire network configuration object. + * @param {object} config The network configuration object. + * @param {object} flowOptions Contains the flow control options for Caliper. + * @param {boolean} discovery Indicates whether discovery is configured or not. + * @param {boolean} gateway Indicates whether gateway mode is configured or not. + */ + static validateNetwork(config, flowOptions, discovery, gateway) { + + // Current limitation is that default Caliper transactions do not work with discovery, since full network knowledge is required; it is required to use a gateway + if (discovery && !gateway) { + throw new Error('Use of discovery is only supported through a gateway transaction'); + } + + // Not possible to use discovery to perform admin operations (init/install) since full knowledge is required + if (discovery && (flowOptions.performInit || flowOptions.performInstall)) { + throw new Error('Use of service discovery is only valid with a `caliper-flow-only-test` flag'); + } + + let tls; // undefined => we don't know yet + // the TLS setting might not be known after the individual section if they are missing + // the first existing node will determine its value, and after that every node is validated against that value + // see the lines: "tls = ... || nodeUrl.startsWith(...);" + + // can't validate mutual TLS now + ConfigValidator._validateTopLevel(config, flowOptions, discovery, gateway, tls); + + // validate CA section + let cas = []; + if (config.certificateAuthorities) { + cas = Object.keys(config.certificateAuthorities); + for (let ca of cas) { + try { + ConfigValidator.validateCertificateAuthority(config.certificateAuthorities[ca], tls); + tls = (tls || false) || config.certificateAuthorities[ca].url.startsWith('https://'); + } catch (err) { + throw new Error(`Invalid "${ca}" CA configuration: ${err.message}`); + } + } + } + + // validate orderer section + let orderers = []; + if (config.orderers) { + orderers = Object.keys(config.orderers); + for (let orderer of orderers) { + try { + ConfigValidator.validateOrderer(config.orderers[orderer], tls); + tls = (tls || false) || config.orderers[orderer].url.startsWith('grpcs://'); + } catch (err) { + throw new Error(`Invalid "${orderer}" orderer configuration: ${err.message}`); + } + } + } + + // validate peer section + let peers = []; + if (config.peers) { + let eventUrl; + peers = Object.keys(config.peers); + for (let peer of peers) { + try { + ConfigValidator.validatePeer(config.peers[peer], tls, eventUrl); + tls = (tls || false) || config.peers[peer].url.startsWith('grpcs://'); + eventUrl = !!config.peers[peer].eventUrl; // the first peer will decide it + } catch (err) { + throw new Error(`Invalid "${peer}" peer configuration: ${err.message}`); + } + } + } + + // validate organization section + let orgs = []; + let mspIds = []; + if (config.organizations) { + orgs = Object.keys(config.organizations); + for (let org of orgs) { + try { + ConfigValidator.validateOrganization(config.organizations[org], peers, cas); + mspIds.push(config.organizations[org].mspid); + } catch (err) { + throw new Error(`Invalid "${org}" organization configuration: ${err.message}`); + } + } + } + + // validate client section + if (config.clients) { + let clients = Object.keys(config.clients); + for (let client of clients) { + try { + ConfigValidator.validateClient(config.clients[client], orgs, config.wallet !== undefined); + } catch (err) { + throw new Error(`Invalid "${client}" client configuration: ${err.message}`); + } + } + } + + // validate channels section + if (config.channels) { + let channels = Object.keys(config.channels); + let takenContractIds = []; + for (let channel of channels) { + try { + ConfigValidator.validateChannel(config.channels[channel], orderers, peers, mspIds, takenContractIds, flowOptions, discovery); + takenContractIds.push(config.channels[channel].chaincodes.map(cc => cc.contractID || cc.id)); + } catch (err) { + throw new Error(`Invalid "${channel}" channel configuration: ${err.message}`); + } + } + } + + // now we can validate mutual TLS + ConfigValidator._validateTopLevel(config, flowOptions, discovery, gateway, tls); + } + + /** + * Validates the top-level properties of the configuration. + * @param {object} config The network configuration object. + * @param {object} flowOptions Contains the flow control options for Caliper. + * @param {boolean} discovery Indicates whether discovery is configured or not. + * @param {boolean} gateway Indicates whether gateway mode is configured or not. + * @param {boolean} tls Indicates whether TLS is enabled or known at this point. + * @private + */ + static _validateTopLevel(config, flowOptions, discovery, gateway, tls) { + // some utility vars for the sake of readability + const onlyScript = !flowOptions.performInit && !flowOptions.performInstall && !flowOptions.performTest; + + // to dynamically call the modifier functions + const modif = onlyScript ? 'optional' : 'required'; + const ordererModif = (onlyScript || discovery) ? 'optional' : 'required'; + + // if server TLS is explicitly disabled, can't enable mutual TLS + let mutualTlsValid = tls === undefined ? [ true, false ] : (tls ? [ true, false ] : [ false ]); + + const schema = j.object().keys({ + // simple attributes + name: j.string().min(1).required(), + version: j.string().valid('1.0').required(), + 'mutual-tls': j.boolean().valid(mutualTlsValid).optional(), + wallet: j.string().min(1).optional(), + caliper: j.object().keys({ + blockchain: j.string().valid('fabric').required(), + command: j.object().keys({ + start: j.string().min(1).optional(), + end: j.string().min(1).optional() + }).or('start', 'end').optional(), + }).required(), + info: j.object().optional(), + + // complicated parts with custom keys + clients: j.object()[modif](), // only required for full workflow + channels: j.object()[modif](), // only required for full workflow + organizations: j.object()[modif](), // only required for full workflow + orderers: j.object()[ordererModif](), // only required for full workflow without discovery + peers: j.object()[modif](), // only required for full workflow + certificateAuthorities: j.object().optional() + }); + + let options = { + abortEarly: false, + allowUnknown: false + }; + let result = j.validate(config, schema, options); + if (result.error) { + throw result.error; + } + } + + /** + * Validates the given channel configuration object + * @param {object} config The configuration object. + * @param {string[]} validOrderers The array of valid orderer names. + * @param {string[]} validPeers The array of valid peer names. + * @param {string[]} validMspIds The array of valid MSP IDs. + * @param {string[]} takenContractIds The array of invalid/taken contract IDs. + * @param {object} flowOptions Contains the flow control options for Caliper. + * @param {boolean} discovery Indicates whether discovery is configured or not. + */ + static validateChannel(config, validOrderers, validPeers, validMspIds, takenContractIds, flowOptions, discovery) { + // ugly hack, but there are too many declarative conditional modifiers otherwise + const created = typeof config.created === 'boolean' ? config.created : false; + const binary = !!config.configBinary; + const def = !!config.definition; + const ordererModif = discovery ? 'optional' : 'required'; + + let binaryModif; + let defModif; + let needXor = false; + let needOptionalXor = false; + + if (!created) { + if (def) { + defModif = 'required'; // definition takes precedence + binaryModif = 'forbidden'; // if it's not specified, this won't matter + } else if (binary) { // && !def + binaryModif = 'required'; + defModif = 'forbidden'; // doesn't matter, it's not specified + } else { + // nothing is specified, so make them optional, but require one + defModif = 'optional'; + binaryModif = 'optional'; + needXor = true; + } + } else { + // nothing is required, but keep the oxor rule if both is specified + defModif = 'optional'; + binaryModif = 'optional'; + needOptionalXor = true; + } + + let createPeersSchema = () => { + let peersSchema = {}; + for (let peer of validPeers) { + peersSchema[peer] = j.object().keys({ + endorsingPeer: j.boolean().optional(), + chaincodeQuery: j.boolean().optional(), + ledgerQuery: j.boolean().optional(), + eventSource: j.boolean().optional(), + }).optional(); + } + + return peersSchema; + }; + + let createEndorsementPolicySchema = () => { + // recursive schema of "X-of" objects + // array element objects either have a "signed-by" key, or a recursive "X-of" + let policySchema = j.array().sparse(false).min(1).items(j.object().min(1) + .pattern(/^signed-by$/, j.number().integer().min(0)) + .pattern(/^[1-9]\d*-of$/, + j.lazy(() => policySchema).description('Policy schema')) + ); + + return j.object().keys({ + identities: j.array().sparse(false).items(j.object().keys({ + role: j.object().keys({ + name: j.string().valid('member', 'admin').required(), + mspId: j.string().valid(validMspIds).required() + }).required() + })).unique().required(), + + // at the top level, allow exactly one "[integer>0]-of" key + // the schema of that top level key will be recursive + policy: j.object().pattern(/^[1-9]\d*-of$/, policySchema).length(1).required() + }); + }; + + let contractIdComparator = (a, b) => { + if (a.contractID) { + if (b.contractID) { + return a.contractID === b.contractID; + } + + return a.contractID === b.id; + } else { + if (b.contractID) { + return a.id === b.contractID; + } + + return a.id === b.id; + } + }; + + const collectionsConfigObjectSchema = j.array().sparse(false).min(1).items(j.object().keys({ + name: j.string().min(1).required(), + policy: createEndorsementPolicySchema().required(), + requiredPeerCount: j.number().integer().min(0).max(j.ref('maxPeerCount')).required(), + maxPeerCount: j.number().integer().min(j.ref('requiredPeerCount')).required(), + blockToLive: j.number().integer().min(0).required() + })).unique('name'); + + let schema = j.object().keys({ + created: j.boolean().optional(), + + configBinary: j.string().min(1)[binaryModif](), + + definition: j.object().keys({ + capabilities: j.array().sparse(false).required(), + consortium: j.string().min(1).required(), + msps: j.array().sparse(false).items(j.string().valid(validMspIds)).unique().required(), + version: j.number().integer().min(0).required() + })[defModif](), + + orderers: j.array().sparse(false).items(j.string().valid(validOrderers)).unique()[ordererModif](), + peers: j.object().keys(createPeersSchema()).required(), + + // leave this embedded, so the validation error messages are more meaningful + chaincodes: j.array().sparse(false).items(j.object().keys({ + id: j.string().min(1).required(), + version: j.string().min(1).required(), + contractID: j.string().min(1).disallow(takenContractIds).optional(), + + language: j.string().valid('golang', 'node', 'java').optional(), + path: j.string().min(1).optional(), + metadataPath: j.string().min(1).optional(), + init: j.array().sparse(false).items(j.string()).optional(), + function: j.string().optional(), + // every key must be a string + initTransientMap: j.object().pattern(j.string(), j.string()).optional(), + + 'collections-config': j.alternatives().try(j.string().min(1), collectionsConfigObjectSchema).optional(), + + 'endorsement-policy': createEndorsementPolicySchema().optional(), + targetPeers: j.array().sparse(false).min(1).unique().items(j.string().valid(validPeers)).optional() + }) // constraints for the chaincode properties + .with('metadataPath', 'path') // if metadataPath is provided, installation needs the path + .with('path', 'language') // if path is provided, installation needs the language + // the following properties indicate instantiation, which needs the language property + .with('init', 'language') + .with('function', 'language') + .with('initTransientMap', 'language') + .with('collections-config', 'language') + .with('endorsement-policy', 'language') + ).unique(contractIdComparator).required() // for the chaincodes collection + }); + + if (needXor) { + schema = schema.xor('configBinary', 'definition'); + } else if (needOptionalXor) { + schema = schema.oxor('configBinary', 'definition'); + } + + let options = { + abortEarly: false, + allowUnknown: false + }; + let result = j.validate(config, schema, options); + if (result.error) { + throw result.error; + } + } + + /** + * Validates the given CA configuration object. + * @param {object} config The configuration object. + * @param {boolean} tls Indicates whether TLS is enabled or known at this point. + */ + static validateCertificateAuthority(config, tls) { + let urlRegex = tls === undefined ? /^(https|http):\/\// : (tls ? /^https:\/\// : /^http:\/\//); + + const schema = j.object().keys({ + url: j.string().uri().regex(urlRegex).required(), + + httpOptions: j.object().optional(), + + // required when using https + tlsCACerts: j.object().keys({ + pem: j.string().min(1).optional(), + path: j.string().min(1).optional() + }).xor('pem', 'path').when('url', { + is: j.string().regex(/^https:\/\//), + then: j.required(), + otherwise: j.forbidden() + }), + + registrar: j.array().items(j.object().keys({ + enrollId: j.string().min(1).required(), + enrollSecret: j.string().min(1).required() + })).min(1).sparse(false).unique('enrollId').required() + }); + + let options = { + abortEarly: false, + allowUnknown: false + }; + let result = j.validate(config, schema, options); + if (result.error) { + throw result.error; + } + } + + /** + * Validates the given peer configuration object. + * @param {object} config The configuration object. + * @param {boolean} tls Indicates whether TLS is enabled or known at this point. + * @param {boolean} eventUrl Indicates whether other peers specified event URLs or not. + */ + static validatePeer(config, tls, eventUrl) { + let urlRegex = tls === undefined ? /^(grpcs|grpc):\/\// : (tls ? /^grpcs:\/\// : /^grpc:\/\//); + let eventModif = eventUrl === undefined ? 'optional' : (eventUrl ? 'required' : 'forbidden'); + + const schema = j.object().keys({ + url: j.string().uri().regex(urlRegex).required(), + // match the protocol of the base "url" + // NOTE: Fabric v1.0.0 can only be detected through the presence of "eventUrl" + eventUrl: j.string().uri().when('url', { + is: j.string().regex(/^grpcs:\/\//), + then: j.string().regex(/^grpcs:\/\//), + otherwise: j.string().regex(/^grpc:\/\//) + })[eventModif](), + grpcOptions: j.object().optional(), + + // required when using https + tlsCACerts: j.object().keys({ + pem: j.string().min(1).optional(), + path: j.string().min(1).optional() + }).xor('pem', 'path').when('url', { + is: j.string().regex(/^grpcs:\/\//), + then: j.required(), + otherwise: j.forbidden() + }) + }); + + let options = { + abortEarly: false, + allowUnknown: false + }; + let result = j.validate(config, schema, options); + if (result.error) { + throw result.error; + } + } + + /** + * Validates the given orderer configuration object. + * @param {object} config The configuration object. + * @param {boolean} tls Indicates whether TLS is enabled or known at this point. + */ + static validateOrderer(config, tls) { + let urlRegex = tls === undefined ? /^(grpcs|grpc):\/\// : (tls ? /^grpcs:\/\// : /^grpc:\/\//); + + const schema = j.object().keys({ + url: j.string().uri().regex(urlRegex).required(), + grpcOptions: j.object().optional(), + + // required when using https + tlsCACerts: j.object().keys({ + pem: j.string().min(1).optional(), + path: j.string().min(1).optional() + }).xor('pem', 'path').when('url', { + is: j.string().regex(/^grpcs:\/\//), + then: j.required(), + otherwise: j.forbidden() + }) + }); + + let options = { + abortEarly: false, + allowUnknown: false + }; + let result = j.validate(config, schema, options); + if (result.error) { + throw result.error; + } + } + + /** + * Validates the given organization configuration object. + * @param {object} config The configuration object. + * @param {string[]} validPeers The array of valid peer names. + * @param {string[]} validCAs The array of valid CA names. + */ + static validateOrganization(config, validPeers, validCAs) { + const schema = j.object().keys({ + mspid: j.string().min(1).required(), + // optional: to include orderer admin clients, and orderer org must be added, which doesn't have peers + peers: j.array().items(j.string().valid(validPeers)) + .min(1).sparse(false).unique().optional(), + certificateAuthorities: j.array().items(j.string().valid(validCAs)) + .min(1).sparse(false).unique().optional(), + + // admin client for orgs are optional + adminPrivateKey: j.object().keys({ + pem: j.string().min(1).optional(), + path: j.string().min(1).optional() + }).xor('pem', 'path').optional(), + + // admin client for orgs are optional + signedCert: j.object().keys({ + pem: j.string().min(1).optional(), + path: j.string().min(1).optional() + }).xor('pem', 'path').optional(), + }).and('adminPrivateKey', 'signedCert'); + + let options = { + abortEarly: false, + allowUnknown: false + }; + let result = j.validate(config, schema, options); + if (result.error) { + throw result.error; + } + } + + /** + * Validates the given client configuration object. + * @param {object} config The configuration object. + * @param {string[]} validOrgs The array of valid organization names. + * @param {boolean} wallet Indicates whether a wallet is configured. + */ + static validateClient(config, validOrgs, wallet) { + const walletModif = wallet ? 'forbidden' : 'optional'; + const credModif = wallet ? 'forbidden' : 'required'; + + let clientSchema = j.object().keys({ + organization: j.string().valid(validOrgs).required(), + // this part is implementation-specific + credentialStore: j.object().keys({ + path: j.string().min(1).required(), + cryptoStore: j.object().keys({ + path: j.string().min(1).required(), + }).required(), + })[credModif](), + + clientPrivateKey: j.object().keys({ + pem: j.string().min(1).optional(), + path: j.string().min(1).optional() + }).xor('pem', 'path')[walletModif](), + + clientSignedCert: j.object().keys({ + pem: j.string().min(1).optional(), + path: j.string().min(1).optional() + }).xor('pem', 'path')[walletModif](), + + affiliation: j.string().min(1)[walletModif](), + attributes: j.array().items(j.object().keys({ + name: j.string().min(1).required(), + value: j.string().required(), + ecert: j.boolean().optional() + })).min(1).sparse(false).unique('name')[walletModif](), + + enrollmentSecret: j.string().min(1)[walletModif](), + + connection: j.object().keys({ + timeout: j.object().keys({ + peer: j.object().keys({ + endorser: j.number().positive().optional(), + eventHub: j.number().positive().optional(), + eventReg: j.number().positive().optional() + }).or('endorser', 'eventHub', 'eventReg').optional(), + orderer: j.number().positive().optional() + }).or('peer', 'orderer').required() + }).optional() + }); + + if (!wallet) { + // additional constraints for the different "client init" methods without wallet + // 1) registration and enrollment specified, attributes require affiliation + // 2) static credentials provided, must be set together + // 3) only a single method can be specified (enrollment-only has no additional constraints) + clientSchema = clientSchema + .with('attributes', 'affiliation') // 1) + // static init/loading + .and('clientPrivateKey', 'clientSignedCert') // 2) + .xor('affiliation', 'enrollmentSecret', 'clientSignedCert'); // 3) + } + + const schema = j.object().keys({ + client: clientSchema.required() + }); + + let options = { + abortEarly: false, + allowUnknown: false + }; + let result = j.validate(config, schema, options); + if (result.error) { + throw result.error; + } + } +} + +module.exports = ConfigValidator; diff --git a/packages/caliper-fabric/lib/fabric.js b/packages/caliper-fabric/lib/fabric.js index 616631781..1659d1adc 100644 --- a/packages/caliper-fabric/lib/fabric.js +++ b/packages/caliper-fabric/lib/fabric.js @@ -21,6 +21,7 @@ const {BlockchainInterface, CaliperUtils, TxStatus, Version, ConfigUtil} = requi const logger = CaliperUtils.getLogger('adapters/fabric'); const FabricNetwork = require('./fabricNetwork.js'); +const ConfigValidator = require('./configValidator.js'); const fs = require('fs'); @@ -138,10 +139,15 @@ class Fabric extends BlockchainInterface { this.workspaceRoot = workspace_root; this.version = new Version(require('fabric-client/package').version); - // NOTE: regardless of the version of the Fabric backend, the SDK must be at least v1.1.0 in order to - // use the common connection profile feature - if (this.version.lessThan('1.1.0')) { - throw new Error(`Fabric SDK ${this.version.toString()} is not supported, use at least version 1.1.0`); + this.network = undefined; + if (typeof networkConfig === 'string') { + let configPath = CaliperUtils.resolvePath(networkConfig, workspace_root); + this.network = CaliperUtils.parseYaml(configPath); + } else if (typeof networkConfig === 'object' && networkConfig !== null) { + // clone the object to prevent modification by other objects + this.network = CaliperUtils.parseYamlString(CaliperUtils.stringifyYaml(networkConfig)); + } else { + throw new Error('[FabricNetwork.constructor] Parameter \'networkConfig\' is neither a file path nor an object'); } this.clientProfiles = new Map(); @@ -150,19 +156,12 @@ class Fabric extends BlockchainInterface { this.eventSources = []; this.clientIndex = 0; this.txIndex = -1; - this.networkUtil = new FabricNetwork(networkConfig, workspace_root); this.randomTargetPeerCache = new Map(); this.channelEventSourcesCache = new Map(); this.randomTargetOrdererCache = new Map(); - this.defaultInvoker = Array.from(this.networkUtil.getClients())[0]; this.wallet = undefined; - this.fileWalletPath = this.networkUtil.fileWalletPath; this.userContracts = new Map(); - if (this.networkUtil.isInCompatibilityMode() && this.version.greaterThan('1.1.0')) { - throw new Error(`Fabric 1.0 compatibility mode is detected, but SDK version ${this.version.toString()} is used`); - } - // this value is hardcoded, if it's used, that means that the provided timeouts are not sufficient this.configSmallestTimeout = 1000; @@ -182,6 +181,23 @@ class Fabric extends BlockchainInterface { this.configLocalHost = ConfigUtil.get(ConfigUtil.keys.Fabric.GatewayLocalHost, true); this.configDiscovery = ConfigUtil.get(ConfigUtil.keys.Fabric.Discovery, false); + ConfigValidator.validateNetwork(this.network, CaliperUtils.getFlowOptions(), + this.configDiscovery, this.configUseGateway); + + this.networkUtil = new FabricNetwork(this.network, workspace_root); + this.fileWalletPath = this.networkUtil.getFileWalletPath(); + this.defaultInvoker = Array.from(this.networkUtil.getClients())[0]; + + // NOTE: regardless of the version of the Fabric backend, the SDK must be at least v1.1.0 in order to + // use the common connection profile feature + if (this.version.lessThan('1.1.0')) { + throw new Error(`Fabric SDK ${this.version.toString()} is not supported, use at least version 1.1.0`); + } + + if (this.networkUtil.isInCompatibilityMode() && this.version.greaterThan('1.1.0')) { + throw new Error(`Fabric 1.0 compatibility mode is detected, but SDK version ${this.version.toString()} is used`); + } + // Network Wallet/Gateway is only available in SDK versions greater than v1.4.0 if ((this.configUseGateway || this.fileWalletPath) && this.version.lessThan('1.4.0')) { throw new Error(`Fabric SDK ${this.version.toString()} is not supported when using a Fabric Gateway object, use at least version 1.4.0`); diff --git a/packages/caliper-fabric/lib/fabricNetwork.js b/packages/caliper-fabric/lib/fabricNetwork.js index b82759097..06ef2a2a0 100644 --- a/packages/caliper-fabric/lib/fabricNetwork.js +++ b/packages/caliper-fabric/lib/fabricNetwork.js @@ -14,10 +14,8 @@ 'use strict'; -const yaml = require('js-yaml'); const fs = require('fs'); const CaliperUtils = require('@hyperledger/caliper-core').CaliperUtils; -const ConfigUtil = require('@hyperledger/caliper-core').ConfigUtil; /** * Utility class for accessing information in a Common Connection Profile configuration @@ -40,13 +38,13 @@ class FabricNetwork { constructor(networkConfig, workspace_root) { CaliperUtils.assertDefined(networkConfig, '[FabricNetwork.constructor] Parameter \'networkConfig\' if undefined or null'); + this.network = undefined; if (typeof networkConfig === 'string') { let configPath = CaliperUtils.resolvePath(networkConfig, workspace_root); - this.network = yaml.safeLoad(fs.readFileSync(configPath, 'utf-8'), - {schema: yaml.DEFAULT_SAFE_SCHEMA}); + this.network = CaliperUtils.parseYaml(configPath); } else if (typeof networkConfig === 'object' && networkConfig !== null) { // clone the object to prevent modification by other objects - this.network = yaml.safeLoad(yaml.safeDump(networkConfig), {schema: yaml.DEFAULT_SAFE_SCHEMA}); + this.network = CaliperUtils.parseYamlString(CaliperUtils.stringifyYaml(networkConfig)); } else { throw new Error('[FabricNetwork.constructor] Parameter \'networkConfig\' is neither a file path nor an object'); } @@ -56,564 +54,134 @@ class FabricNetwork { this.tls = false; this.mutualTls = false; this.contractMapping = new Map(); - this.workspaceRoot = workspace_root; - this._validateNetworkConfiguration(); + this._processConfiguration(workspace_root); } - //////////////////////////////// - // INTERNAL UTILITY FUNCTIONS // - //////////////////////////////// /** - * Internal utility function for checking whether the given CA exists in the configuration. - * The function throws an error if it doesn't exist. - * @param {string} ca The name of the CA. - * @param {string} msg An optional error message that will be thrown in case of an invalid CA. - * @private - */ - _assertCaExists(ca, msg) { - let cas = this.network.certificateAuthorities; - for (let c in cas) { - if (!cas.hasOwnProperty(c)) { - continue; - } - - // we found the CA in the CAs section - if (c.toString() === ca) { - return; - } - } - - // didn't find the CA - throw new Error(msg || `Couldn't find ${ca} in the 'certificateAuthorities' section`); - } - - /** - * Internal utility function for checking whether the given orderer exists in the configuration. - * The function throws an error if it doesn't exist. - * @param {string} orderer The name of the orderer. - * @param {string} msg An optional error message that will be thrown in case of an invalid orderer. - * @private - */ - _assertOrdererExists(orderer, msg) { - let orderers = this.network.orderers; - for (let ord in orderers) { - if (!orderers.hasOwnProperty(ord)) { - continue; - } - - // we found the orderer in the orderers section - if (ord.toString() === orderer) { - return; - } - } - - // didn't find the orderer - throw new Error(msg || `Couldn't find ${orderer} in the 'orderers' section`); - } - - /** - * Internal utility function for checking whether the given peer exists in the configuration. - * The function throws an error if it doesn't exist. - * @param {string} peer The name of the peer. - * @param {string} msg An optional error message that will be thrown in case of an invalid peer. - * @private - */ - _assertPeerExists(peer, msg) { - let peers = this.network.peers; - for (let p in peers) { - if (!peers.hasOwnProperty(p)) { - continue; - } - - // we found the peer in the peers section - if (p.toString() === peer) { - return; - } - } - - // didn't find the peer - throw new Error(msg || `Couldn't find ${peer} in the 'peers' section`); - } - - /** - * Internal utility function for validating that the Common Connection Profile - * setting contains every required property. + * Internal utility function for retrieving key information from the network configuration. + * @param {string} workspaceRoot The path to the root of the workspace. * * @private */ - _validateNetworkConfiguration() { - // Validation requirements are modified based on: - // - flow options - // - use of Fabric discovery service - const flowOptions = CaliperUtils.getFlowOptions(); - const discovery = ConfigUtil.get(ConfigUtil.keys.Fabric.Discovery, false); - - // If only running start/end commands, we can return immediately - if (!flowOptions.performInit && !flowOptions.performInstall && !flowOptions.performTest) { - return; - } + _processConfiguration(workspaceRoot) { + this.mutualTls = !!this.network['mutual-tls']; - // Current limitation is that default Caliper transactions do not work with discovery, since full network knowedge is required; it is required to use a gateway - if (discovery && !ConfigUtil.get(ConfigUtil.keys.Fabric.Gateway, false)) { - throw new Error('Use of discovery is only supported through a gateway transaction'); + if (this.network.wallet) { + this.network.wallet = CaliperUtils.resolvePath(this.network.wallet, workspaceRoot); } - // Not possible to use discovery to perform admin operations (init/install) since full knowledge is required - if (discovery && (flowOptions.performInit || flowOptions.performInstall)) { - throw new Error('Use of service discovery is only valid with a `caliper-flow-only-test` flag'); - } - - // Top level properties - // CAs are only needed when a user enrollment or registration is needed - let requiredCas = new Set(); - let providedCas = new Set(); - - if (discovery) { - // Orderers requirement removed when using discovery - CaliperUtils.assertAllProperties(this.network, 'network', - 'caliper', 'clients', 'channels', 'organizations', 'peers'); - } else { - CaliperUtils.assertAllProperties(this.network, 'network', - 'caliper', 'clients', 'channels', 'organizations', 'orderers', 'peers'); - } - - CaliperUtils.assertProperty(this.network.caliper, 'network.caliper', 'blockchain'); - - this.mutualTls = CaliperUtils.checkProperty(this.network, 'mutual-tls') ? this.network['mutual-tls'] : false; - this.fileWalletPath = CaliperUtils.checkProperty(this.network, 'wallet') ? this.network.wallet : false; - - // =========== - // = CLIENTS = - // =========== - - let clients = this.getClients(); - if (clients.size < 1) { - throw new Error('Network configuration error: the \'clients\' section does not contain any entries'); - } - - for (let client of clients) { - let clientObjectName = `network.clients.${client}`; - let clientObject = this.network.clients[client]; - - CaliperUtils.assertProperty(clientObject, clientObjectName, 'client'); - this.clientConfigs[client] = this.network.clients[client]; - - // include the client level for convenience - clientObject = this.network.clients[client].client; - clientObjectName = `network.clients.${client}.client`; - - CaliperUtils.assertProperty(clientObject, clientObjectName, 'organization'); - - // Will be using either a wallet with existing identities, or keys/certs for the creation of new identities - if (!this.fileWalletPath) { - CaliperUtils.assertProperty(clientObject, clientObjectName, 'credentialStore'); - CaliperUtils.assertAllProperties(clientObject.credentialStore, `${clientObjectName}.credentialStore`, 'path', 'cryptoStore'); - CaliperUtils.assertProperty(clientObject.credentialStore.cryptoStore, `${clientObjectName}.credentialStore.cryptoStore`, 'path'); + if (this.network.clients) { + let clients = this.getClients(); + for (let client of clients) { + this.clientConfigs[client] = this.network.clients[client]; + let cObj = this.network.clients[client].client; // normalize paths - clientObject.credentialStore.path = CaliperUtils.resolvePath(clientObject.credentialStore.path, this.workspaceRoot); - clientObject.credentialStore.cryptoStore.path = CaliperUtils.resolvePath(clientObject.credentialStore.cryptoStore.path, this.workspaceRoot); - - // user identity can be provided in multiple ways - // if there is any crypto content info, every crypto content info is needed - if (CaliperUtils.checkAnyProperty(clientObject, 'clientPrivateKey', 'clientSignedCert')) { - CaliperUtils.assertAllProperties(clientObject, clientObjectName, 'clientPrivateKey', 'clientSignedCert'); - - // either file path or pem content is needed - CaliperUtils.assertAnyProperty(clientObject.clientPrivateKey, `${clientObjectName}.clientPrivateKey`, 'path', 'pem'); - CaliperUtils.assertAnyProperty(clientObject.clientSignedCert, `${clientObjectName}.clientSignedCert`, 'path', 'pem'); + if (cObj.credentialStore) { + cObj.credentialStore.path = CaliperUtils.resolvePath(cObj.credentialStore.path, workspaceRoot); + cObj.credentialStore.cryptoStore.path = CaliperUtils.resolvePath(cObj.credentialStore.cryptoStore.path, workspaceRoot); + } - // normalize the paths if provided - if (CaliperUtils.checkProperty(clientObject.clientPrivateKey, 'path')) { - clientObject.clientPrivateKey.path = CaliperUtils.resolvePath(clientObject.clientPrivateKey.path, this.workspaceRoot); - } + if (cObj.clientPrivateKey && cObj.clientPrivateKey.path) { + cObj.clientPrivateKey.path = CaliperUtils.resolvePath(cObj.clientPrivateKey.path, workspaceRoot); + } - if (CaliperUtils.checkProperty(clientObject.clientSignedCert, 'path')) { - clientObject.clientSignedCert.path = CaliperUtils.resolvePath(clientObject.clientSignedCert.path, this.workspaceRoot); - } - } else if (CaliperUtils.checkProperty(clientObject, 'enrollSecret')) { - // otherwise, enrollment info can also be specified and the CA will be needed - // TODO: currently only one CA is supported - requiredCas.add(this.getOrganizationOfClient(client)); - } else { - // if no crypto material or enrollment info is provided, then registration and CA info is needed - CaliperUtils.assertProperty(clientObject, clientObjectName, 'affiliation'); - // TODO: currently only one CA is supported - requiredCas.add(this.getOrganizationOfClient(client)); + if (cObj.clientSignedCert && cObj.clientSignedCert.path) { + cObj.clientSignedCert.path = CaliperUtils.resolvePath(cObj.clientSignedCert.path, workspaceRoot); } } } - // ============ - // = CHANNELS = - // ============ - let channels = this.getChannels(); - if (channels.size < 1) { - throw new Error('Network configuration error: the \'channels\' section does not contain any entries'); - } - for (let channel of channels) { - let channelObj = this.network.channels[channel]; - let channelObjName = `network.channels.${channel}`; - - // mandatory top-level property for benchmark operation - CaliperUtils.assertAllProperties(channelObj, channelObjName, 'chaincodes'); - - if (flowOptions.performInit) { - // Init stage requires channel configuration artifacts - if (!CaliperUtils.checkProperty(channelObj, 'created') || !channelObj.created) { - // one kind of config is needed - binary or definition - if (!CaliperUtils.checkProperty(channelObj, 'configBinary')) { - CaliperUtils.assertAllProperties(channelObj, channelObjName, 'definition'); - } - } - - // mandatory top-level properties in order to create channels - CaliperUtils.assertAllProperties(channelObj, channelObjName, 'orderers', 'peers'); - - // ==================== - // = CHANNEL ORDERERS = - // ==================== - let ordererCollection = channelObj.orderers; - - if (ordererCollection.length < 1) { - throw new Error(`'channels.${channel}.orderers' does not contain any element`); - } + if (this.network.channels) { + let channels = this.getChannels(); + for (let channel of channels) { + let cObj = this.network.channels[channel]; - // check whether the orderer references are valid - ordererCollection.forEach((orderer) => { - this._assertOrdererExists(orderer, `${orderer} is not a valid orderer reference in ${channel}`); - }); - - // ================= - // = CHANNEL PEERS = - // ================= - let peerCollection = channelObj.peers; - let peerPresent = false; // signal if we found a peer in the channel - - // check whether the peer references are valid - for (let peer in peerCollection) { - if (!peerCollection.hasOwnProperty(peer)) { - continue; + for (let cc of cObj.chaincodes) { + if (!cc.contractID) { + cc.contractID = cc.id; } - this._assertPeerExists(peer, - `${peer.toString()} is not a valid peer reference in ${channel}`); - peerPresent = true; - } - - if (!peerPresent) { - throw new Error(`'channels.${channel}.peers' does not contain any element`); + this.contractMapping.set(cc.contractID, {channel: channel, id: cc.id, version: cc.version}); + if (cc.language && cc.language !== 'golang' && cc.path) { + cc.path = CaliperUtils.resolvePath(cc.path, workspaceRoot); + } } } - - // ====================== - // = CHANNEL CHAINCODES = - // ====================== - let chaincodesCollection = channelObj.chaincodes; - if (chaincodesCollection.size < 1) { - throw new Error(`'channels.${channel}.chaincodes' does not contain any elements`); - } - - // to check that there's no duplication - let chaincodeSet = new Set(); - - chaincodesCollection.forEach((cc, index) => { - // 'metadataPath', 'targetPeers' and 'init' is optional - CaliperUtils.assertDefined(cc, `The element 'channels.${channel}.chaincodes[${index}]' is undefined or null`); - - // other attributes are optional if the chaincode is already installed and instantiated - // this will be know at install/instantiate time - CaliperUtils.assertAllProperties(cc, `channels.${channel}.chaincodes[${index}]`, 'id', 'version'); - - let idAndVersion = `${cc.id}@${cc.version}`; - if (chaincodeSet.has(idAndVersion)) { - throw new Error(`${idAndVersion} is defined more than once in the configuration`); - } - - let contractID; - if (CaliperUtils.checkProperty(cc, 'contractID')) { - contractID = cc.contractID; - } else { - contractID = cc.id; - } - - if (this.contractMapping.has(contractID)) { - throw new Error(`Contract ID ${contractID} is used more than once in the configuration`); - } - - // add the mapping for the contract ID - this.contractMapping.set(contractID, {channel: channel, id: cc.id, version: cc.version}); - - chaincodeSet.add(idAndVersion); - - // if target peers are defined, then check the validity of the references - if (!CaliperUtils.checkProperty(cc, 'targetPeers')) { - return; - } - - for (let tp of cc.targetPeers) { - this._assertPeerExists(tp, - `${tp} is not a valid peer reference in 'channels.${channel}.chaincodes[${index}].targetPeers'`); - } - }); } - // ================= - // = ORGANIZATIONS = - // ================= - let orgs = this.getOrganizations(); - if (orgs.size < 1) { - throw new Error('\'organizations\' section does not contain any entries'); - } + if (this.network.organizations) { + let orgs = this.getOrganizations(); + for (let org of orgs) { + let oObj = this.network.organizations[org]; - for (let org of orgs) { - let orgObj = this.network.organizations[org]; - let orgObjName = `network.organizations.${org}`; - - // Caliper is a special client, it requires admin access to every org - // NOTE: because of the queries during the init phase, we can't avoid using admin profiles - // CAs are only needed if a user needs to be enrolled or registered - if (this.fileWalletPath) { - // Credentials exist within wallet - CaliperUtils.assertAllProperties(orgObj, orgObjName, 'mspid', 'peers'); - } else { - CaliperUtils.assertAllProperties(orgObj, orgObjName, 'mspid', 'peers', 'adminPrivateKey', 'signedCert'); - // either path or pem is required - CaliperUtils.assertAnyProperty(orgObj.adminPrivateKey, `${orgObjName}.adminPrivateKey`, 'path', 'pem'); - CaliperUtils.assertAnyProperty(orgObj.signedCert, `${orgObjName}.signedCert`, 'path', 'pem'); - - // normalize paths if provided - if (CaliperUtils.checkProperty(orgObj.adminPrivateKey, 'path')) { - orgObj.adminPrivateKey.path = CaliperUtils.resolvePath(orgObj.adminPrivateKey.path, this.workspaceRoot); + if (oObj.adminPrivateKey && oObj.adminPrivateKey.path) { + oObj.adminPrivateKey.path = CaliperUtils.resolvePath(oObj.adminPrivateKey.path, workspaceRoot); } - if (CaliperUtils.checkProperty(orgObj.signedCert, 'path')) { - orgObj.signedCert.path = CaliperUtils.resolvePath(orgObj.signedCert.path, this.workspaceRoot); + if (oObj.signedCert && oObj.signedCert.path) { + oObj.signedCert.path = CaliperUtils.resolvePath(oObj.signedCert.path, workspaceRoot); } } - - // ====================== - // = ORGANIZATION PEERS = - // ====================== - if (orgObj.peers.length < 1) { - throw new Error(`'organizations.${org}.peers' does not contain any element`); - } - - // verify peer references - for (let peer of orgObj.peers) { - this._assertPeerExists(peer, `${peer} is not a valid peer reference in 'organizations.${org}.peers'`); - } - - // =================== - // = ORGANIZATION CA = - // =================== - - // if CAs are specified, check their validity - if (!CaliperUtils.checkProperty(orgObj, 'certificateAuthorities')) { - continue; - } - - let caCollection = orgObj.certificateAuthorities; - for (let ca of caCollection) { - this._assertCaExists(ca, `${ca} is not a valid CA reference in 'organizations.${org}'.certificateAuthorities`); - } } - // ============ - // = ORDERERS = - // ============ - if (!discovery) { + if (this.network.orderers) { let orderers = this.getOrderers(); - if (orderers.size < 1) { - throw new Error('\'orderers\' section does not contain any entries'); - } - for (let orderer of orderers) { - // 'grpcOptions' is optional - CaliperUtils.assertProperty(this.network.orderers, 'network.orderers', orderer); - let ordererObj = this.network.orderers[orderer]; - let ordererObjName = `network.orderers.${orderer}`; - - CaliperUtils.assertProperty(ordererObj, ordererObjName, 'url'); - // tlsCACerts is needed only for TLS - if (ordererObj.url.startsWith('grpcs://')) { - this.tls = true; - // CaliperUtils.assertProperty(ordererObj, ordererObjName, 'tlsCACerts'); - // CaliperUtils.assertAnyProperty(ordererObj.tlsCACerts, `${ordererObjName}.tlsCACerts`, 'path', 'pem'); - - // normalize path is provided - if (CaliperUtils.checkProperty(ordererObj.tlsCACerts, 'path')) { - ordererObj.tlsCACerts.path = CaliperUtils.resolvePath(ordererObj.tlsCACerts.path, this.workspaceRoot); - } - } - } - } + let oObj = this.network.orderers[orderer]; - // ========= - // = PEERS = - // ========= - let peers = this.getPeers(); - if (peers.size < 1) { - throw new Error('\'peers\' section does not contain any entries'); - } - for (let peer of peers) { - // 'grpcOptions' is optional - CaliperUtils.assertProperty(this.network.peers, 'network.peers', peer); - let peerObj = this.network.peers[peer]; - let peerObjName = `network.peers.${peer}`; - - CaliperUtils.assertProperty(peerObj, peerObjName, 'url'); - - // tlsCACerts is needed only for TLS - if (peerObj.url.startsWith('grpcs://')) { - this.tls = true; - if (!this.fileWalletPath) { - CaliperUtils.assertProperty(peerObj, peerObjName, 'tlsCACerts'); - CaliperUtils.assertAnyProperty(peerObj.tlsCACerts, `${peerObjName}.tlsCACerts`, 'path', 'pem'); - - // normalize path if provided - if (CaliperUtils.checkProperty(peerObj.tlsCACerts, 'path')) { - peerObj.tlsCACerts.path = CaliperUtils.resolvePath(peerObj.tlsCACerts.path, this.workspaceRoot); - } - } - } + this.tls |= oObj.url.startsWith('grpcs://'); - if (CaliperUtils.checkProperty(peerObj, 'eventUrl')) { - this.compatibilityMode = true; - - // check if both URLS are using TLS or neither - if ((peerObj.url.startsWith('grpcs://') && peerObj.eventUrl.startsWith('grpc://')) || - (peerObj.url.startsWith('grpc://') && peerObj.eventUrl.startsWith('grpcs://'))) { - throw new Error(`${peer} uses different protocols for the transaction and event services`); + if (oObj.tlsCACerts && oObj.tlsCACerts.path) { + oObj.tlsCACerts.path = CaliperUtils.resolvePath(oObj.tlsCACerts.path, workspaceRoot); } } } - // in case of compatibility mode, require event URLs from every peer - if (this.compatibilityMode) { + if (this.network.peers) { + let peers = this.getPeers(); for (let peer of peers) { - if (!CaliperUtils.checkProperty(this.network.peers[peer], 'eventUrl')) { - throw new Error(`${peer} doesn't provide an event URL in compatibility mode`); - } - } - } + let pObj = this.network.peers[peer]; - // =========================== - // = CERTIFICATE AUTHORITIES = - // =========================== - if (CaliperUtils.checkProperty(this.network, 'certificateAuthorities')) { - let cas = this.getCertificateAuthorities(); - for (let ca of cas) { - // 'httpOptions' is optional - CaliperUtils.assertProperty(this.network.certificateAuthorities, 'network.certificateAuthorities', ca); - - let caObj = this.network.certificateAuthorities[ca]; - let caObjName = `network.certificateAuthorities.${ca}`; - - // validate the registrars if provided - if (CaliperUtils.checkProperty(caObj, 'registrar')) { - caObj.registrar.forEach((reg, index) => { - CaliperUtils.assertAllProperties(caObj.registrar[index], `${caObjName}.registrar[${index}]`, 'enrollId', 'enrollSecret'); - }); + this.tls |= pObj.url.startsWith('grpcs://'); - // we actually need the registrar, not just the CA - providedCas.add(this.getOrganizationOfCertificateAuthority(ca)); + if (pObj.tlsCACerts && pObj.tlsCACerts.path) { + pObj.tlsCACerts.path = CaliperUtils.resolvePath(pObj.tlsCACerts.path, workspaceRoot); } - // tlsCACerts is needed only for TLS - if (caObj.url.startsWith('https://') && !this.fileWalletPath) { - this.tls = true; - CaliperUtils.assertProperty(caObj, caObjName, 'tlsCACerts'); - CaliperUtils.assertAnyProperty(caObj.tlsCACerts, `${caObjName}.tlsCACerts`, 'path', 'pem'); - - //normalize path if provided - if (CaliperUtils.checkProperty(caObj.tlsCACerts, 'path')) { - caObj.tlsCACerts.path = CaliperUtils.resolvePath(caObj.tlsCACerts.path, this.workspaceRoot); - } + if (pObj.eventUrl) { + this.compatibilityMode = true; } } } - // find the not provided CAs, i.e., requiredCas \ providedCas set operation - let notProvidedCas = new Set([...requiredCas].filter(ca => !providedCas.has(ca))); - if (notProvidedCas.size > 0) { - throw new Error(`The following org's CAs and their registrars are required for user management, but are not provided: ${Array.from(notProvidedCas).join(', ')}`); - } - - // ============================== - // = CHECK CONSISTENT TLS USAGE = - // ============================== - - // if at least one node has TLS configured - if (this.tls) { - if (!discovery) { - // check every orderer - const orderers = this.getOrderers(); - for (let orderer of orderers) { - let ordererObj = this.network.orderers[orderer]; - let ordererObjName = `network.orderers.${orderer}`; - - CaliperUtils.assertProperty(ordererObj, ordererObjName, 'tlsCACerts'); - CaliperUtils.assertAnyProperty(ordererObj.tlsCACerts, `${ordererObjName}.tlsCACerts`, 'path', 'pem'); - - if (!ordererObj.url.startsWith('grpcs://')) { - throw new Error(`${orderer} doesn't use the grpcs protocol, but TLS is configured on other nodes`); - } - } - } - - // check every peer - for (let peer of peers) { - let peerObj = this.network.peers[peer]; - let peerObjName = `network.peers.${peer}`; - - if (!this.fileWalletPath) { - CaliperUtils.assertProperty(peerObj, peerObjName, 'tlsCACerts'); - CaliperUtils.assertAnyProperty(peerObj.tlsCACerts, `${peerObjName}.tlsCACerts`, 'path', 'pem'); - } - - if (!peerObj.url.startsWith('grpcs://')) { - throw new Error(`${peer} doesn't use the grpcs protocol, but TLS is configured on other nodes`); - } - - // check peer URLs - if (this.compatibilityMode && !peerObj.eventUrl.startsWith('grpcs://')) { - throw new Error(`${peer} doesn't use the grpcs protocol for eventing, but TLS is configured on other nodes`); - } - } - - // check every CA - if (CaliperUtils.checkProperty(this.network, 'certificateAuthorities')) { - let cas = this.getCertificateAuthorities(); - for (let ca of cas) { - let caObj = this.network.certificateAuthorities[ca]; - let caObjName = `network.certificateAuthorities.${ca}`; + if (this.network.certificateAuthorities) { + let cas = this.getCertificateAuthorities(); + for (let ca of cas) { + let caObj = this.network.certificateAuthorities[ca]; - if (!this.fileWalletPath) { - CaliperUtils.assertProperty(caObj, caObjName, 'tlsCACerts'); - CaliperUtils.assertAnyProperty(caObj.tlsCACerts, `${caObjName}.tlsCACerts`, 'path', 'pem'); - } + this.tls |= caObj.url.startsWith('https://'); - if (!caObj.url.startsWith('https://')) { - throw new Error(`${ca} doesn't use the https protocol, but TLS is configured on other nodes`); - } + if (caObj.tlsCACerts && caObj.tlsCACerts.path) { + caObj.tlsCACerts.path = CaliperUtils.resolvePath(caObj.tlsCACerts.path, workspaceRoot); } } } - // else: none of the nodes indicated TLS in their configuration/protocol, so nothing to check - - // mutual TLS requires server-side TLS - if (this.mutualTls && !this.tls) { - throw new Error('Mutual TLS is configured without using TLS on network nodes'); - } - if (this.mutualTls && this.compatibilityMode) { throw new Error('Mutual TLS is not supported for Fabric v1.0'); } } + /** + * Gets the configured file wallet path. + * @return {string} The file wallet path, or false if omitted. + */ + getFileWalletPath() { + return this.network.wallet || false; + } + /** * Gets the admin crypto materials for the given organization. * @param {string} org The name of the organization. @@ -634,13 +202,13 @@ class FabricNetwork { let signedCertPEM; if (CaliperUtils.checkProperty(privateKey, 'path')) { - privateKeyPEM = fs.readFileSync(CaliperUtils.resolvePath(privateKey.path, this.workspaceRoot)); + privateKeyPEM = fs.readFileSync(privateKey.path); } else { privateKeyPEM = privateKey.pem; } if (CaliperUtils.checkProperty(signedCert, 'path')) { - signedCertPEM = fs.readFileSync(CaliperUtils.resolvePath(signedCert.path, this.workspaceRoot)); + signedCertPEM = fs.readFileSync(signedCert.path); } else { signedCertPEM = signedCert.pem; } @@ -656,10 +224,6 @@ class FabricNetwork { }; } - ////////////////////// - // PUBLIC FUNCTIONS // - ////////////////////// - /** * Gets the affiliation of the given client. * @param {string} client The client name. @@ -809,13 +373,13 @@ class FabricNetwork { let signedCertPEM; if (CaliperUtils.checkProperty(privateKey, 'path')) { - privateKeyPEM = fs.readFileSync(CaliperUtils.resolvePath(privateKey.path, this.workspaceRoot)); + privateKeyPEM = fs.readFileSync(privateKey.path); } else { privateKeyPEM = privateKey.pem; } if (CaliperUtils.checkProperty(signedCert, 'path')) { - signedCertPEM = fs.readFileSync(CaliperUtils.resolvePath(signedCert.path, this.workspaceRoot)); + signedCertPEM = fs.readFileSync(signedCert.path); } else { signedCertPEM = signedCert.pem; } @@ -976,7 +540,7 @@ class FabricNetwork { * @returns {object} The network configuration object. */ getNewNetworkObject() { - return yaml.safeLoad(yaml.safeDump(this.network)); + return CaliperUtils.parseYamlString(CaliperUtils.stringifyYaml(this.network)); } /** @@ -1268,7 +832,7 @@ class FabricNetwork { let tlsPEM; if (CaliperUtils.checkProperty(tlsCACert, 'path')) { - tlsPEM = fs.readFileSync(CaliperUtils.resolvePath(tlsCACert.path, this.workspaceRoot)).toString(); + tlsPEM = fs.readFileSync(tlsCACert.path).toString(); } else { tlsPEM = tlsCACert.pem; } diff --git a/packages/caliper-fabric/package.json b/packages/caliper-fabric/package.json index 2d6c3fec5..aaed3f031 100644 --- a/packages/caliper-fabric/package.json +++ b/packages/caliper-fabric/package.json @@ -8,7 +8,7 @@ "scripts": { "pretest": "npm run licchk", "licchk": "license-check-and-add", - "test": "npm run lint", + "test": "npm run lint && npm run nyc", "lint": "npx eslint .", "nyc": "nyc mocha --recursive -t 10000" }, @@ -22,6 +22,7 @@ "src/comm/template/report.html" ], "dependencies": { + "@hapi/joi": "^15.1.1", "@hyperledger/caliper-core": "0.1.0" }, "devDependencies": { @@ -72,19 +73,19 @@ } }, "nyc": { - "exclude": [ - "lib/**" + "include": [ + "lib/configValidator.js" ], "reporter": [ "text-summary", "html" ], "all": true, - "check-coverage": false, - "statements": 5, - "branches": 8, - "functions": 7, - "lines": 5 + "check-coverage": true, + "statements": 1, + "branches": 1, + "functions": 1, + "lines": 1 }, "license": "Apache-2.0", "licenses": [ diff --git a/packages/caliper-fabric/test/configValidator.js b/packages/caliper-fabric/test/configValidator.js new file mode 100644 index 000000000..1cabaad30 --- /dev/null +++ b/packages/caliper-fabric/test/configValidator.js @@ -0,0 +1,3674 @@ +/* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +const rewire = require('rewire'); +const ConfigValidator = rewire('../lib/configValidator.js'); + +const chai = require('chai'); +chai.should(); + +const arrow = '\u21B3'; + +/** + * Returns the property name prefixed with an arrow symbol. + * @param {string} propertyName The text to prefix. + * @return {string} The prefixed text. + */ +function prop(propertyName) { + return `${arrow} ${propertyName}`; +} + +describe('Class: ConfigValidator', () => { + // General remarks: + // - variables "outside" the "it" functions are always reset at the appropriate hierarchy + // - variables "outside" the "it" functions are always initialized to assist code completion + // - every test category/hierarchy starts with a test for accepting the valid configuration + // - the tests are grouped by according to the config property hierarchy + + // these vars have the same structure for every test, thus can be global + let flowOptions = { + performStart: true, + performInit: true, + performInstall: true, + performTest: true, + performEnd: true + }; + + let discovery = false; + let gateway = false; + let tls = undefined; + + // reset the global vars before every test + beforeEach(() => { + flowOptions = { + performStart: true, + performInit: true, + performInstall: true, + performTest: true, + performEnd: true + }; + + discovery = false; + gateway = false; + tls = undefined; + }); + + describe('Function: validateNetwork', () => { + let config = { + name: 'Fabric', + version: '1.0', + 'mutual-tls': false, + caliper: { + blockchain: 'fabric' + }, + clients: { + 'client0.org1.example.com': { + client: { + organization: 'Org1', + credentialStore: { + path: 'path', + cryptoStore: { + path: 'path' + } + }, + + clientPrivateKey: { + path: 'path' + }, + clientSignedCert: { + path: 'path' + } + } + }, + 'client0.org2.example.com': { + client: { + organization: 'Org2', + credentialStore: { + path: 'path', + cryptoStore: { + path: 'path' + } + }, + + clientPrivateKey: { + path: 'path' + }, + clientSignedCert: { + path: 'path' + } + } + } + }, + channels: { + channel1: { + created: false, + configBinary: 'path', + orderers: ['orderer.example.com'], + peers: { + 'peer0.org1.example.com': {}, + 'peer0.org2.example.com': {} + }, + chaincodes: [ { id: 'drm', version: 'v0' } ] + }, + channel2: { + created: false, + configBinary: 'path', + orderers: ['orderer.example.com'], + peers: { + 'peer0.org1.example.com': {}, + 'peer0.org2.example.com': {} + }, + chaincodes: [ { id: 'drm', contractID: 'drm2', version: 'v0' } ] + } + }, + organizations: { + Org1: { + mspid: 'Org1MSP', + peers: [ + 'peer0.org1.example.com' + ], + certificateAuthorities: [ + 'ca.org1.example.com' + ] + }, + Org2: { + mspid: 'Org2MSP', + peers: [ + 'peer0.org2.example.com' + ], + certificateAuthorities: [ + 'ca.org2.example.com' + ] + } + }, + orderers: { + 'orderer.example.com': { + url: 'grpcs://localhost:7051', + tlsCACerts: { + path: 'my/path/tocert' + } + } + }, + peers: { + 'peer0.org1.example.com': { + url: 'grpcs://localhost:7051', + tlsCACerts: { + path: 'my/path/tocert' + } + }, + 'peer0.org2.example.com': { + url: 'grpcs://localhost:7051', + tlsCACerts: { + path: 'my/path/tocert' + } + } + }, + certificateAuthorities: { + 'ca.org1.example.com': { + url: 'https://localhost:7054', + tlsCACerts: { + path: 'my/path/tocert' + }, + registrar: [ + { enrollId: 'admin1', enrollSecret: 'secret1' }, + { enrollId: 'admin2', enrollSecret: 'secret2' } + ] + }, + 'ca.org2.example.com': { + url: 'https://localhost:7054', + tlsCACerts: { + path: 'my/path/tocert' + }, + registrar: [ + { enrollId: 'admin1', enrollSecret: 'secret1' }, + { enrollId: 'admin2', enrollSecret: 'secret2' } + ] + } + } + }; + let configString = JSON.stringify(config); + + beforeEach(() => { + config = JSON.parse(configString); + }); + + /** + * Wraps the actual call, so "should" can call this function without parameters + */ + function call() { + ConfigValidator.validateNetwork(config, flowOptions, discovery, gateway); + } + + it('should not throw for a valid value', () => { + call.should.not.throw(); + }); + + describe('Flow consistency', () => { + it('should throw when using discovery without the gateway mode', () => { + const err = 'Use of discovery is only supported through a gateway transaction'; + discovery = true; + gateway = false; + call.should.throw(err); + }); + + it('should throw when using discovery with init phase', () => { + const err = 'Use of service discovery is only valid with a `caliper-flow-only-test` flag'; + discovery = true; + gateway = true; + flowOptions.performStart = flowOptions.performInstall = flowOptions.performTest = flowOptions.performEnd = false; + call.should.throw(err); + }); + + it('should throw when using discovery with install phase', () => { + const err = 'Use of service discovery is only valid with a `caliper-flow-only-test` flag'; + discovery = true; + gateway = true; + flowOptions.performStart = flowOptions.performInit = flowOptions.performTest = flowOptions.performEnd = false; + call.should.throw(err); + }); + + it('should not throw when omitting top sections in script-only flow', () => { + flowOptions.performInit = flowOptions.performInstall = flowOptions.performTest = false; + delete config.certificateAuthorities; + delete config.clients; + delete config.peers; + delete config.orderers; + delete config.organizations; + delete config.channels; + call.should.not.throw(); + }); + + it('should detect incorrect peer TLS based on orderer TLS', () => { + const err = 'Invalid "peer0.org1.example.com" peer configuration: child "url" fails because ["url" with value "grpc://localhost:7051" fails to match the required pattern: /^grpcs:\\/\\//]'; + delete config.certificateAuthorities; + delete config.organizations.Org1.certificateAuthorities; + delete config.organizations.Org2.certificateAuthorities; + delete config.peers['peer0.org1.example.com'].tlsCACerts; + config.peers['peer0.org1.example.com'].url = 'grpc://localhost:7051'; + call.should.throw(err); + }); + + it('should detect incorrect peer TLS based on other peer when CAs and orderers are missing', () => { + const err = 'Invalid "peer0.org2.example.com" peer configuration: child "url" fails because ["url" with value "grpc://localhost:7051" fails to match the required pattern: /^grpcs:\\/\\//]'; + flowOptions.performStart = flowOptions.performInit = flowOptions.performInstall = flowOptions.performEnd = false; + discovery = true; + gateway = true; + delete config.certificateAuthorities; + delete config.organizations.Org1.certificateAuthorities; + delete config.organizations.Org2.certificateAuthorities; + delete config.orderers; + delete config.channels.channel1.orderers; + delete config.channels.channel2.orderers; + delete config.peers['peer0.org2.example.com'].tlsCACerts; + config.peers['peer0.org2.example.com'].url = 'grpc://localhost:7051'; + call.should.throw(err); + }); + + }); + + describe('Peer references', () => { + it('should throw when a non-existing peer is referenced in an organization', () => { + const err = 'Invalid "Org1" organization configuration: child "peers" fails because ["peers" at position 1 fails because ["1" must be one of [peer0.org1.example.com, peer0.org2.example.com]]]'; + config.organizations.Org1.peers.push('peer5.org1.example.com'); + call.should.throw(err); + }); + + it('should throw when a non-existing peer is referenced in a channel', () => { + const err = 'Invalid "channel1" channel configuration: child "peers" fails because ["peer5.org1.example.com" is not allowed]'; + config.channels.channel1.peers['peer5.org1.example.com'] = {}; + call.should.throw(err); + }); + + it('should throw when a non-existing peer is referenced in a chaincode', () => { + const err = 'Invalid "channel1" channel configuration: child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "targetPeers" fails because ["targetPeers" at position 0 fails because ["0" must be one of [peer0.org1.example.com, peer0.org2.example.com]]]]]'; + config.channels.channel1.chaincodes[0].targetPeers = ['peer5.org1.example.com']; + call.should.throw(err); + }); + }); + + describe('Orderer references', () => { + it('should throw when a non-existing orderer is referenced in a channel', () => { + const err = 'Invalid "channel1" channel configuration: child "orderers" fails because ["orderers" at position 1 fails because ["1" must be one of [orderer.example.com]]]'; + config.channels.channel1.orderers.push('orderer5.example.com'); + call.should.throw(err); + }); + }); + + describe('CA references', () => { + it('should throw when a non-existing CA is referenced in an organization', () => { + const err = 'Invalid "Org1" organization configuration: child "certificateAuthorities" fails because ["certificateAuthorities" at position 1 fails because ["1" must be one of [ca.org1.example.com, ca.org2.example.com]]]'; + config.organizations.Org1.certificateAuthorities.push('ca5.org1.example.com'); + call.should.throw(err); + }); + }); + + describe('MSP ID references', () => { + it('should throw when a non-existing MSP ID is referenced in a channel definition ', () => { + const err = 'Invalid "channel1" channel configuration: child "definition" fails because [child "msps" fails because ["msps" at position 1 fails because ["1" must be one of [Org1MSP, Org2MSP]]]]'; + delete config.channels.channel1.configBinary; + config.channels.channel1.definition = { + capabilities : [], + consortium : 'SampleConsortium', + msps : [ 'Org1MSP', 'Org5MSP' ], + version : 0 + }; + call.should.throw(err); + }); + + it('should throw when a non-existing MSP ID is referenced in a chaincode endorsement policy', () => { + const err = 'Invalid "channel1" channel configuration: child "chaincodes" fails because ["chaincodes" at position 1 fails because [child "endorsement-policy" fails because [child "identities" fails because ["identities" at position 0 fails because [child "role" fails because [child "mspId" fails because ["mspId" must be one of [Org1MSP, Org2MSP]]]]]]]]'; + config.channels.channel1.chaincodes.push({ + id: 'marbles', + contractID: 'ContractMarbles', + version: 'v0', + language: 'golang', + path: 'path', + 'endorsement-policy': { + identities: [ + { role: { name: 'member', mspId: 'Org1MSP' }}, + { role: { name: 'member', mspId: 'Org2MSP' }} + ], + policy: { + '2-of': [ + { 'signed-by': 1}, + { '1-of': [{ 'signed-by': 0 }, { 'signed-by': 1 }]} + ] + } + } + }); + + config.channels.channel1.chaincodes[1]['endorsement-policy'].identities[0].role.mspId = 'Org5MSP'; + call.should.throw(err); + }); + + it('should throw when a non-existing MSP ID is referenced in a chaincode collections config policy', () => { + const err = 'Invalid "channel1" channel configuration: child "chaincodes" fails because ["chaincodes" at position 1 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "identities" fails because ["identities" at position 0 fails because [child "role" fails because [child "mspId" fails because ["mspId" must be one of [Org1MSP, Org2MSP]]]]]]]]]]'; + config.channels.channel1.chaincodes.push({ + id: 'marbles', + contractID: 'ContractMarbles', + version: 'v0', + language: 'golang', + path: 'path', + 'collections-config': [{ + name: 'name', + policy: { + identities: [ + {role: {name: 'member', mspId: 'Org1MSP'}}, + {role: {name: 'member', mspId: 'Org2MSP'}} + ], + policy: { + '2-of': [ + {'signed-by': 1}, + {'1-of': [{'signed-by': 0}, {'signed-by': 1}]} + ] + } + }, + requiredPeerCount: 1, + maxPeerCount: 2, + blockToLive: 0 + }] + }); + + config.channels.channel1.chaincodes[1]['collections-config'][0].policy.identities[0].role.mspId = 'Org5MSP'; + call.should.throw(err); + }); + }); + + describe('TLS consistency', () => { + it('should throw for inconsistent TLS protocol in CAs', () => { + const err = 'Invalid "ca.org2.example.com" CA configuration: child "url" fails because ["url" with value "https://localhost:7054" fails to match the required pattern: /^http:\\/\\//]'; + delete config.certificateAuthorities['ca.org1.example.com'].tlsCACerts; + config.certificateAuthorities['ca.org1.example.com'].url = 'http://localhost:7054'; + call.should.throw(err); + }); + + it('should throw for inconsistent TLS protocol in peers', () => { + const err = 'Invalid "peer0.org1.example.com" peer configuration: child "url" fails because ["url" with value "grpc://localhost:7051" fails to match the required pattern: /^grpcs:\\/\\//]'; + delete config.peers['peer0.org1.example.com'].tlsCACerts; + config.peers['peer0.org1.example.com'].url = 'grpc://localhost:7051'; + call.should.throw(err); + }); + + it('should throw for inconsistent TLS protocol in peer eventing', () => { + const err = 'Invalid "peer0.org1.example.com" peer configuration: child "eventUrl" fails because ["eventUrl" with value "grpc://localhost:7051" fails to match the required pattern: /^grpcs:\\/\\//]'; + config.peers['peer0.org1.example.com'].eventUrl = 'grpc://localhost:7051'; + call.should.throw(err); + }); + + it('should throw for inconsistent TLS protocol in orderers', () => { + const err = 'Invalid "orderer.example.com" orderer configuration: child "url" fails because ["url" with value "grpc://localhost:7051" fails to match the required pattern: /^grpcs:\\/\\//]'; + delete config.orderers['orderer.example.com'].tlsCACerts; + config.orderers['orderer.example.com'].url = 'grpc://localhost:7051'; + call.should.throw(err); + }); + }); + + describe('Peer eventing consistency', () => { + it('should throw for inconsistent event URL usage among peers', () => { + const err = 'Invalid "peer0.org2.example.com" peer configuration: child "eventUrl" fails because ["eventUrl" is required]'; + config.peers['peer0.org1.example.com'].eventUrl = 'grpcs://localhost:7053'; + call.should.throw(err); + }); + }); + + describe('Informative errors', () => { + it('should throw an informative error about invalid client configuration', () => { + const err = 'Invalid "client0.org1.example.com" client configuration: "affiliation" is not allowed'; + config.clients['client0.org1.example.com'].affiliation = 'aff1'; + call.should.throw(err); + }); + }); + }); + + describe('Function: _validateTopLevel', () => { + // good practice for auto complete and easy backup + let config = { + name: 'Fabric', + version: '1.0', + 'mutual-tls': false, + wallet: '/path', + caliper: { + blockchain: 'fabric', + command: { start: 'start command', end: 'end command' } + }, + info: { info1: 'some info' }, + clients: { client1: {} }, + channels: { channel1: {} }, + organizations: { org1: {} }, + orderers: { orderer1: {} }, + peers: { peer1: {} }, + certificateAuthorities: { ca1: {} } + }; + let configString = JSON.stringify(config); + + beforeEach(() => { + config = JSON.parse(configString); + }); + + /** + * Wraps the actual call, so "should" can call this function without parameters + */ + function call() { + ConfigValidator._validateTopLevel(config, flowOptions, discovery, gateway, tls); + } + + it('should not throw for a valid value', () => { + call.should.not.throw(); + }); + + it('should throw for unknown child property', () => { + const err = '"unknown" is not allowed'; + config.unknown = ''; + call.should.throw(err); + }); + + describe(prop('name'), () => { + it('should throw for missing required property', () => { + const err = 'child "name" fails because ["name" is required]'; + delete config.name; + call.should.throw(err); + }); + + it('should throw for an empty string value', () => { + const err = 'child "name" fails because ["name" is not allowed to be empty, "name" length must be at least 1 characters long]'; + config.name = ''; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "name" fails because ["name" must be a string]'; + config.name = true; + call.should.throw(err); + }); + }); + + describe(prop('version'), () => { + it('should throw for missing required property', () => { + const err = 'child "version" fails because ["version" is required]'; + delete config.version; + call.should.throw(err); + }); + + it('should throw for an invalid string value', () => { + const err = 'child "version" fails because ["version" must be one of [1.0]]'; + config.version = '2.0'; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "version" fails because ["version" must be a string]'; + config.version = true; + call.should.throw(err); + }); + }); + + describe(prop('mutual-tls'), () => { + it('should not throw for missing optional property', () => { + delete config['mutual-tls']; + call.should.not.throw(); + }); + + it('should not throw for any valid value when TLS is not known', () => { + config['mutual-tls'] = false; + call.should.not.throw(); + config['mutual-tls'] = true; + call.should.not.throw(); + }); + + it('should throw for a non-boolean value', () => { + const err = 'child "mutual-tls" fails because ["mutual-tls" must be a boolean]'; + config['mutual-tls'] = 'yes'; + call.should.throw(err); + }); + + it('should not throw when set to "true" with server TLS', () => { + tls = true; + config['mutual-tls'] = true; + call.should.not.throw(); + }); + + it('should throw when set to "true" without server TLS', () => { + const err = 'child "mutual-tls" fails because ["mutual-tls" must be one of [false]]'; + tls = false; + config['mutual-tls'] = true; + call.should.throw(err); + }); + }); + + describe(prop('wallet'), () => { + it('should not throw for missing optional property', () => { + delete config.wallet; + call.should.not.throw(); + }); + + it('should throw for an empty string value', () => { + const err = 'child "wallet" fails because ["wallet" is not allowed to be empty, "wallet" length must be at least 1 characters long]'; + config.wallet = ''; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "wallet" fails because ["wallet" must be a string]'; + config.wallet = true; + call.should.throw(err); + }); + }); + + describe(prop('caliper'), () => { + const err = 'child "caliper" fails because ["caliper" is required]'; + it('should throw for missing required property', () => { + delete config.caliper; + call.should.throw(err); + }); + + it('should throw for non-object value', () => { + const err = 'child "caliper" fails because ["caliper" must be an object]'; + config.caliper = ''; + call.should.throw(err); + }); + + it('should throw for unknown child property', () => { + const err = 'child "caliper" fails because ["unknown" is not allowed]'; + config.caliper.unknown = ''; + call.should.throw(err); + }); + + describe(prop('blockchain'), () => { + it('should throw for missing required property', () => { + const err = 'child "caliper" fails because [child "blockchain" fails because ["blockchain" is required]]'; + delete config.caliper.blockchain; + call.should.throw(err); + }); + + it('should throw for an invalid string value', () => { + const err = 'child "caliper" fails because [child "blockchain" fails because ["blockchain" must be one of [fabric]]]'; + config.caliper.blockchain = 'sawtooth'; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "caliper" fails because [child "blockchain" fails because ["blockchain" must be a string]]'; + config.caliper.blockchain = true; + call.should.throw(err); + }); + }); + + describe(prop('command'), () => { + it('should throw for a non-object value', () => { + const err = 'child "caliper" fails because [child "command" fails because ["command" must be an object]]'; + config.caliper.command = ''; + call.should.throw(err); + }); + + it('should not throw for missing optional property', () => { + delete config.caliper.command; + call.should.not.throw(); + }); + + it('should throw for an empty object value', () => { + const err = 'child "caliper" fails because [child "command" fails because ["value" must contain at least one of [start, end]]]'; + delete config.caliper.command.start; + delete config.caliper.command.end; + call.should.throw(err); + }); + + it('should throw for unknown child property', () => { + const err = 'child "caliper" fails because [child "command" fails because ["unknown" is not allowed]]'; + config.caliper.command.unknown = ''; + call.should.throw(err); + }); + + it('should not throw for only missing the optional "start" child property', () => { + delete config.caliper.command.start; + call.should.not.throw(); + }); + + it('should not throw for only missing the optional "end" child property', () => { + delete config.caliper.command.end; + call.should.not.throw(); + }); + + describe(prop('start'), () => { + it('should throw for an empty string value', () => { + const err = 'child "caliper" fails because [child "command" fails because [child "start" fails because ["start" is not allowed to be empty, "start" length must be at least 1 characters long]]]'; + config.caliper.command.start = ''; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "caliper" fails because [child "command" fails because [child "start" fails because ["start" must be a string]]]'; + config.caliper.command.start = true; + call.should.throw(err); + }); + }); + + describe(prop('end'), () => { + it('should throw for an empty string property', () => { + const err = 'child "caliper" fails because [child "command" fails because [child "end" fails because ["end" is not allowed to be empty, "end" length must be at least 1 characters long]]]'; + config.caliper.command.end = ''; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "caliper" fails because [child "command" fails because [child "end" fails because ["end" must be a string]]]'; + config.caliper.command.end = true; + call.should.throw(err); + }); + }); + }); + }); + + describe(prop('info'), () => { + it('should not throw for missing optional property', () => { + delete config.info; + call.should.not.throw(); + }); + + it('should not throw for empty value', () => { + config.info = {}; + call.should.not.throw(); + }); + + it('should throw for a non-object value', () => { + const err = 'child "info" fails because ["info" must be an object]'; + config.info = 'yes'; + call.should.throw(err); + }); + }); + + describe(prop('clients'), () => { + it('should throw for missing required property', () => { + const err = 'child "clients" fails because ["clients" is required]'; + delete config.clients; + call.should.throw(err); + }); + + it('should throw for a non-object value', () => { + const err = 'child "clients" fails because ["clients" must be an object]'; + config.clients = 'yes'; + call.should.throw(err); + }); + + it('should not throw for missing property when only scripts are executed', () => { + flowOptions.performInit = flowOptions.performInstall = flowOptions.performTest = false; + delete config.clients; + call.should.not.throw(); + }); + }); + + describe(prop('channels'), () => { + it('should throw for missing required property', () => { + const err = 'child "channels" fails because ["channels" is required]'; + delete config.channels; + call.should.throw(err); + }); + + it('should throw for a non-object value', () => { + const err = 'child "channels" fails because ["channels" must be an object]'; + config.channels = 'yes'; + call.should.throw(err); + }); + + it('should not throw for missing property when only scripts are executed', () => { + flowOptions.performInit = flowOptions.performInstall = flowOptions.performTest = false; + delete config.channels; + call.should.not.throw(); + }); + }); + + describe(prop('organizations'), () => { + it('should throw for missing required property', () => { + const err = 'child "organizations" fails because ["organizations" is required]'; + delete config.organizations; + call.should.throw(err); + }); + + it('should throw for a non-object value', () => { + const err = 'child "organizations" fails because ["organizations" must be an object]'; + config.organizations = 'yes'; + call.should.throw(err); + }); + + it('should not throw for missing property when only scripts are executed', () => { + flowOptions.performInit = flowOptions.performInstall = flowOptions.performTest = false; + delete config.organizations; + call.should.not.throw(); + }); + }); + + describe(prop('orderers'), () => { + it('should throw for missing required property', () => { + const err = 'child "orderers" fails because ["orderers" is required]'; + delete config.orderers; + call.should.throw(err); + }); + + it('should throw for a non-object value', () => { + const err = 'child "orderers" fails because ["orderers" must be an object]'; + config.orderers = 'yes'; + call.should.throw(err); + }); + + it('should not throw for missing property when only scripts are executed', () => { + flowOptions.performInit = flowOptions.performInstall = flowOptions.performTest = false; + delete config.orderers; + call.should.not.throw(); + }); + + it('should not throw for missing property in discovery mode', () => { + discovery = true; + delete config.orderers; + call.should.not.throw(); + }); + }); + + describe(prop('peers'), () => { + it('should throw for missing required property', () => { + const err = 'child "peers" fails because ["peers" is required]'; + delete config.peers; + call.should.throw(err); + }); + + it('should throw for a non-object value', () => { + const err = 'child "peers" fails because ["peers" must be an object]'; + config.peers = 'yes'; + call.should.throw(err); + }); + + it('should not throw for missing property when only scripts are executed', () => { + flowOptions.performInit = flowOptions.performInstall = flowOptions.performTest = false; + delete config.peers; + call.should.not.throw(); + }); + }); + + describe(prop('certificateAuthorities'), () => { + it('should not throw for missing optional property', () => { + delete config.certificateAuthorities; + call.should.not.throw(); + }); + + it('should throw for a non-object value', () => { + const err = 'child "certificateAuthorities" fails because ["certificateAuthorities" must be an object]'; + config.certificateAuthorities = 'yes'; + call.should.throw(err); + }); + }); + }); + + describe('Function: validateCertificateAuthority', () => { + let config = { + url: 'https://localhost:7054', + httpOptions: { + verify: false + }, + tlsCACerts: { + path: 'my/path/tocert' + }, + registrar: [ + { enrollId: 'admin1', enrollSecret: 'secret1' }, + { enrollId: 'admin2', enrollSecret: 'secret2' } + ] + }; + let configString = JSON.stringify(config); + + // reset the local config before every test + beforeEach(() => { + config = JSON.parse(configString); + }); + + /** + * Wraps the actual call, so "should" can call this function without parameters + */ + function call() { + ConfigValidator.validateCertificateAuthority(config, tls); + } + + it('should not throw for a valid value', () => { + call.should.not.throw(); + }); + + it('should throw for an unknown child property', () => { + const err = '"unknown" is not allowed'; + config.unknown = ''; + call.should.throw(err); + }); + + describe(prop('url'), () => { + it('should throw for missing required property', () => { + const err = 'child "url" fails because ["url" is required]'; + delete config.url; + call.should.throw(err); + }); + + it('should throw for an empty string value', () => { + const err = 'child "url" fails because ["url" is not allowed to be empty, "url" must be a valid uri, "url" with value "" fails to match the required pattern: /^(https|http):\\/\\//]'; + delete config.tlsCACerts; + config.url = ''; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "url" fails because ["url" must be a string]'; + delete config.tlsCACerts; + config.url = true; + call.should.throw(err); + }); + + it('should throw for a wrong protocol value', () => { + const err = 'child "url" fails because ["url" with value "grpc://localhost:7054" fails to match the required pattern: /^(https|http):\\/\\//]'; + delete config.tlsCACerts; + config.url = 'grpc://localhost:7054'; + call.should.throw(err); + }); + + it('should throw for a non-URI value', () => { + const err = 'child "url" fails because ["url" must be a valid uri, "url" with value "invalid" fails to match the required pattern: /^(https|http):\\/\\//]'; + delete config.tlsCACerts; + config.url = 'invalid'; + call.should.throw(err); + }); + + it('should not throw for any valid protocol value when TLS is not known', () => { + config.url = 'https://localhost:7054'; + call.should.not.throw(); + + delete config.tlsCACerts; + config.url = 'http://localhost:7054'; + call.should.not.throw(); + }); + + it('should throw for a non-TLS protocol when TLS is set', () => { + const err = 'child "url" fails because ["url" with value "http://localhost:7054" fails to match the required pattern: /^https:\\/\\//]'; + tls = true; + delete config.tlsCACerts; + config.url = 'http://localhost:7054'; + call.should.throw(err); + }); + + it('should throw for a TLS protocol when TLS is not set', () => { + const err = 'child "url" fails because ["url" with value "https://localhost:7054" fails to match the required pattern: /^http:\\/\\//]'; + tls = false; + config.url = 'https://localhost:7054'; + call.should.throw(err); + }); + }); + + describe(prop('httpOptions'), () => { + it('should not throw for missing optional property', () => { + delete config.httpOptions; + call.should.not.throw(); + }); + + it('should throw for a non-object value', () => { + const err = 'child "httpOptions" fails because ["httpOptions" must be an object]'; + config.httpOptions = 'yes'; + call.should.throw(err); + }); + }); + + describe(prop('tlsCACerts'), () => { + it('should throw for a non-object value', () => { + const err = 'child "tlsCACerts" fails because ["tlsCACerts" must be an object]'; + config.tlsCACerts = 'yes'; + call.should.throw(err); + }); + + it('should throw for an empty value', () => { + const err = 'child "tlsCACerts" fails because ["value" must contain at least one of [pem, path]]'; + config.tlsCACerts = {}; + call.should.throw(err); + }); + + it('should throw for missing required property when using TLS', () => { + const err = 'child "tlsCACerts" fails because ["tlsCACerts" is required]'; + delete config.tlsCACerts; + call.should.throw(err); + }); + + it('should not throw for missing property when not using TLS', () => { + delete config.tlsCACerts; + config.url = 'http://localhost:7054'; + call.should.not.throw(); + }); + + it('should throw for forbidden property when not using TLS', () => { + const err = 'child "tlsCACerts" fails because ["tlsCACerts" is not allowed]'; + config.url = 'http://localhost:7054'; + call.should.throw(err); + }); + + it('should throw when setting both "path" and "pem" child properties', () => { + const err = 'child "tlsCACerts" fails because ["value" contains a conflict between exclusive peers [pem, path]]'; + config.tlsCACerts.pem = 'asdf'; + call.should.throw(err); + }); + + it('should throw for unknown child property', () => { + const err = 'child "tlsCACerts" fails because ["unknown" is not allowed]'; + config.tlsCACerts.unknown = ''; + call.should.throw(err); + }); + + describe(prop('path'), () => { + it('should throw for an empty string value', () => { + const err = 'child "tlsCACerts" fails because [child "path" fails because ["path" is not allowed to be empty, "path" length must be at least 1 characters long]]'; + config.tlsCACerts.path = ''; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "tlsCACerts" fails because [child "path" fails because ["path" must be a string]]'; + config.tlsCACerts.path = true; + call.should.throw(err); + }); + }); + + describe(prop('pem'), () => { + beforeEach(() => { + delete config.tlsCACerts.path; + }); + + it('should not throw when setting property instead of sibling "path" property', () => { + config.tlsCACerts.pem = 'asdf'; + call.should.not.throw(); + }); + + it('should throw for a non-string value', () => { + const err = 'child "tlsCACerts" fails because [child "pem" fails because ["pem" must be a string]]'; + config.tlsCACerts.pem = true; + call.should.throw(err); + }); + + it('should throw for an empty string value', () => { + const err = 'child "tlsCACerts" fails because [child "pem" fails because ["pem" is not allowed to be empty, "pem" length must be at least 1 characters long]]'; + config.tlsCACerts.pem = ''; + call.should.throw(err); + }); + }); + }); + + describe(prop('registrar'), () => { + it('should throw for missing required property', () => { + const err = 'child "registrar" fails because ["registrar" is required]'; + delete config.registrar; + call.should.throw(err); + }); + + it('should throw for a non-array value', () => { + const err = 'child "registrar" fails because ["registrar" must be an array]'; + config.registrar = 'yes'; + call.should.throw(err); + }); + + it('should throw for an empty array value', () => { + const err = 'child "registrar" fails because ["registrar" must contain at least 1 items]'; + config.registrar = []; + call.should.throw(err); + }); + + it('should throw for an undefined array item', () => { + const err = 'child "registrar" fails because ["registrar" must not be a sparse array]'; + config.registrar[2] = undefined; + call.should.throw(err); + }); + + describe(prop('[item].enrollId'), () => { + it('should throw for an item with a non-string value', () => { + const err = 'child "registrar" fails because ["registrar" at position 0 fails because [child "enrollId" fails because ["enrollId" must be a string]]]'; + config.registrar[0].enrollId = true; + call.should.throw(err); + }); + + it('should throw for an item with an empty string value', () => { + const err = 'child "registrar" fails because ["registrar" at position 0 fails because [child "enrollId" fails because ["enrollId" is not allowed to be empty, "enrollId" length must be at least 1 characters long]]]'; + config.registrar[0].enrollId = ''; + call.should.throw(err); + }); + + it('should throw for an item with a duplicate value', () => { + const err = 'child "registrar" fails because ["registrar" position 1 contains a duplicate value]'; + config.registrar[1].enrollId = 'admin1'; + call.should.throw(err); + }); + }); + + describe(prop('[item].enrollSecret'), () => { + it('should throw for an item with a non-string value', () => { + const err = 'child "registrar" fails because ["registrar" at position 0 fails because [child "enrollSecret" fails because ["enrollSecret" must be a string]]]'; + config.registrar[0].enrollSecret = true; + call.should.throw(err); + }); + + it('should throw for an item with an empty string value', () => { + const err = 'child "registrar" fails because ["registrar" at position 0 fails because [child "enrollSecret" fails because ["enrollSecret" is not allowed to be empty, "enrollSecret" length must be at least 1 characters long]]]'; + config.registrar[0].enrollSecret = ''; + call.should.throw(err); + }); + }); + }); + }); + + describe('Function: validatePeer', () => { + let config = { + url: 'grpcs://localhost:7051', + eventUrl: 'grpcs://localhost:7053', + grpcOptions: { + 'ssl-target-name-override': 'peer0.org1.example.com', + }, + tlsCACerts: { + path: 'my/path/tocert' + } + }; + + let eventUrl = true; + let configString = JSON.stringify(config); + + beforeEach(() => { + config = JSON.parse(configString); + eventUrl = true; + }); + + /** + * Wraps the actual call, so "should" can call this function without parameters + */ + function call() { + ConfigValidator.validatePeer(config, tls, eventUrl); + } + + it('should not throw for a valid value', () => { + call.should.not.throw(); + }); + + it('should throw for an unknown child property', () => { + const err = '"unknown" is not allowed'; + config.unknown = ''; + call.should.throw(err); + }); + + describe(prop('url'), () => { + it('should throw for missing required property', () => { + const err = 'child "url" fails because ["url" is required]'; + delete config.url; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "url" fails because ["url" must be a string]. child "eventUrl" fails because ["eventUrl" with value "grpcs://localhost:7053" fails to match the required pattern: /^grpc:\\/\\//]'; + delete config.tlsCACerts; + config.url = true; + call.should.throw(err); + }); + + it('should throw for a wrong protocol value', () => { + const err = 'child "url" fails because ["url" with value "https://localhost:7054" fails to match the required pattern: /^(grpcs|grpc):\\/\\//]'; + delete config.tlsCACerts; + delete config.eventUrl; + config.url = 'https://localhost:7054'; + call.should.throw(err); + }); + + it('should throw for a non-URI value', () => { + const err = 'child "url" fails because ["url" must be a valid uri, "url" with value "invalid" fails to match the required pattern: /^(grpcs|grpc):\\/\\//]'; + delete config.tlsCACerts; + delete config.eventUrl; + config.url = 'invalid'; + call.should.throw(err); + }); + + it('should not throw for any valid protocol value when TLS is not known', () => { + config.url = 'grpcs://localhost:7054'; + call.should.not.throw(); + + delete config.tlsCACerts; + config.eventUrl = 'grpc://localhost:7054'; + config.url = 'grpc://localhost:7054'; + call.should.not.throw(); + }); + + it('should throw for a non-TLS protocol when TLS is set', () => { + const err = 'child "url" fails because ["url" with value "grpc://localhost:7054" fails to match the required pattern: /^grpcs:\\/\\//]'; + tls = true; + delete config.tlsCACerts; + config.eventUrl = 'grpc://localhost:7054'; + config.url = 'grpc://localhost:7054'; + call.should.throw(err); + }); + + it('should throw for a TLS protocol when TLS is not set', () => { + const err = 'child "url" fails because ["url" with value "grpcs://localhost:7054" fails to match the required pattern: /^grpc:\\/\\//]'; + tls = false; + config.url = 'grpcs://localhost:7054'; + call.should.throw(err); + }); + }); + + describe(prop('eventUrl'), () => { + it('should not throw for missing optional property', () => { + eventUrl = false; + delete config.eventUrl; + call.should.not.throw(); + }); + + it('should throw for missing property when other peers also set it', () => { + const err = 'child "eventUrl" fails because ["eventUrl" is required]'; + delete config.eventUrl; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "eventUrl" fails because ["eventUrl" must be a string]'; + config.eventUrl = true; + call.should.throw(err); + }); + + it('should throw for a non-URI value', () => { + const err = 'child "eventUrl" fails because ["eventUrl" must be a valid uri, "eventUrl" with value "invalid" fails to match the required pattern: /^grpcs:\\/\\//]'; + config.eventUrl = 'invalid'; + call.should.throw(err); + }); + + it('should throw for a mismatching protocol value', () => { + const err = 'child "eventUrl" fails because ["eventUrl" with value "grpc://localhost:7054" fails to match the required pattern: /^grpcs:\\/\\//]'; + config.eventUrl = 'grpc://localhost:7054'; + call.should.throw(err); + }); + }); + + describe(prop('grpcOptions'), () => { + it('should not throw for missing optional property', () => { + delete config.grpcOptions; + call.should.not.throw(); + }); + + it('should throw for a non-object value', () => { + const err = 'child "grpcOptions" fails because ["grpcOptions" must be an object]'; + config.grpcOptions = 'yes'; + call.should.throw(err); + }); + }); + + describe(prop('tlsCACerts'), () => { + it('should throw for a non-object value', () => { + const err = 'child "tlsCACerts" fails because ["tlsCACerts" must be an object]'; + config.tlsCACerts = 'yes'; + call.should.throw(err); + }); + + it('should throw for an empty value', () => { + const err = 'child "tlsCACerts" fails because ["value" must contain at least one of [pem, path]]'; + config.tlsCACerts = {}; + call.should.throw(err); + }); + + it('should throw for missing required property when using TLS', () => { + const err = 'child "tlsCACerts" fails because ["tlsCACerts" is required]'; + delete config.tlsCACerts; + call.should.throw(err); + }); + + it('should not throw for missing property when not using TLS', () => { + delete config.tlsCACerts; + config.url = 'grpc://localhost:7054'; + config.eventUrl = 'grpc://localhost:7054'; + call.should.not.throw(); + }); + + it('should throw for forbidden property when not using TLS', () => { + const err = 'child "tlsCACerts" fails because ["tlsCACerts" is not allowed]'; + config.url = 'grpc://localhost:7054'; + config.eventUrl = 'grpc://localhost:7054'; + call.should.throw(err); + }); + + it('should throw when setting both "path" and "pem" child properties', () => { + const err = 'child "tlsCACerts" fails because ["value" contains a conflict between exclusive peers [pem, path]]'; + config.tlsCACerts.pem = 'asdf'; + call.should.throw(err); + }); + + it('should throw for unknown child property', () => { + const err = 'child "tlsCACerts" fails because ["unknown" is not allowed]'; + config.tlsCACerts.unknown = ''; + call.should.throw(err); + }); + + describe(prop('path'), () => { + it('should throw for an empty string value', () => { + const err = 'child "tlsCACerts" fails because [child "path" fails because ["path" is not allowed to be empty, "path" length must be at least 1 characters long]]'; + config.tlsCACerts.path = ''; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "tlsCACerts" fails because [child "path" fails because ["path" must be a string]]'; + config.tlsCACerts.path = true; + call.should.throw(err); + }); + }); + + describe(prop('pem'), () => { + beforeEach(() => { + delete config.tlsCACerts.path; + }); + + it('should not throw when setting property instead of sibling "path" property', () => { + config.tlsCACerts.pem = 'asdf'; + call.should.not.throw(); + }); + + it('should throw for a non-string value', () => { + const err = 'child "tlsCACerts" fails because [child "pem" fails because ["pem" must be a string]]'; + config.tlsCACerts.pem = true; + call.should.throw(err); + }); + + it('should throw for an empty string value', () => { + const err = 'child "tlsCACerts" fails because [child "pem" fails because ["pem" is not allowed to be empty, "pem" length must be at least 1 characters long]]'; + config.tlsCACerts.pem = ''; + call.should.throw(err); + }); + }); + }); + }); + + describe('Function: validateOrderer', () => { + let config = { + url: 'grpcs://localhost:7051', + grpcOptions: { + 'ssl-target-name-override': 'orderer.example.com', + }, + tlsCACerts: { + path: 'my/path/tocert' + } + }; + let configString = JSON.stringify(config); + + beforeEach(() => { + config = JSON.parse(configString); + }); + + /** + * Wraps the actual call, so "should" can call this function without parameters + */ + function call() { + ConfigValidator.validateOrderer(config, tls); + } + + it('should not throw for a valid value', () => { + call.should.not.throw(); + }); + + it('should throw for an unknown child property', () => { + const err = '"unknown" is not allowed'; + config.unknown = ''; + call.should.throw(err); + }); + + describe(prop('url'), () => { + it('should throw for missing required property', () => { + const err = 'child "url" fails because ["url" is required]'; + delete config.url; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "url" fails because ["url" must be a string]'; + delete config.tlsCACerts; + config.url = true; + call.should.throw(err); + }); + + it('should throw for a wrong protocol value', () => { + const err = 'child "url" fails because ["url" with value "https://localhost:7054" fails to match the required pattern: /^(grpcs|grpc):\\/\\//]'; + delete config.tlsCACerts; + config.url = 'https://localhost:7054'; + call.should.throw(err); + }); + + it('should throw for a non-URI value', () => { + const err = 'child "url" fails because ["url" must be a valid uri, "url" with value "invalid" fails to match the required pattern: /^(grpcs|grpc):\\/\\//]'; + delete config.tlsCACerts; + config.url = 'invalid'; + call.should.throw(err); + }); + + it('should not throw for any valid protocol value when TLS is not known', () => { + config.url = 'grpcs://localhost:7054'; + call.should.not.throw(); + + delete config.tlsCACerts; + config.url = 'grpc://localhost:7054'; + call.should.not.throw(); + }); + + it('should throw for a non-TLS protocol when TLS is set', () => { + const err = 'child "url" fails because ["url" with value "grpc://localhost:7054" fails to match the required pattern: /^grpcs:\\/\\//]'; + tls = true; + delete config.tlsCACerts; + config.url = 'grpc://localhost:7054'; + call.should.throw(err); + }); + + it('should throw for a TLS protocol when TLS is not set', () => { + const err = 'child "url" fails because ["url" with value "grpcs://localhost:7054" fails to match the required pattern: /^grpc:\\/\\//]'; + tls = false; + config.url = 'grpcs://localhost:7054'; + call.should.throw(err); + }); + }); + + describe(prop('grpcOptions'), () => { + it('should not throw for missing optional property', () => { + delete config.grpcOptions; + call.should.not.throw(); + }); + + it('should throw for a non-object value', () => { + const err = 'child "grpcOptions" fails because ["grpcOptions" must be an object]'; + config.grpcOptions = 'yes'; + call.should.throw(err); + }); + }); + + describe(prop('tlsCACerts'), () => { + it('should throw for a non-object value', () => { + const err = 'child "tlsCACerts" fails because ["tlsCACerts" must be an object]'; + config.tlsCACerts = 'yes'; + call.should.throw(err); + }); + + it('should throw for an empty value', () => { + const err = 'child "tlsCACerts" fails because ["value" must contain at least one of [pem, path]]'; + config.tlsCACerts = {}; + call.should.throw(err); + }); + + it('should throw for missing required property when using TLS', () => { + const err = 'child "tlsCACerts" fails because ["tlsCACerts" is required]'; + delete config.tlsCACerts; + call.should.throw(err); + }); + + it('should not throw for missing property when not using TLS', () => { + delete config.tlsCACerts; + config.url = 'grpc://localhost:7054'; + call.should.not.throw(); + }); + + it('should throw for forbidden property when not using TLS', () => { + const err = 'child "tlsCACerts" fails because ["tlsCACerts" is not allowed]'; + config.url = 'grpc://localhost:7054'; + call.should.throw(err); + }); + + it('should throw when setting both "path" and "pem" child properties', () => { + const err = 'child "tlsCACerts" fails because ["value" contains a conflict between exclusive peers [pem, path]]'; + config.tlsCACerts.pem = 'asdf'; + call.should.throw(err); + }); + + it('should throw for unknown child property', () => { + const err = 'child "tlsCACerts" fails because ["unknown" is not allowed]'; + config.tlsCACerts.unknown = ''; + call.should.throw(err); + }); + + describe(prop('path'), () => { + it('should throw for an empty string value', () => { + const err = 'child "tlsCACerts" fails because [child "path" fails because ["path" is not allowed to be empty, "path" length must be at least 1 characters long]]'; + config.tlsCACerts.path = ''; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "tlsCACerts" fails because [child "path" fails because ["path" must be a string]]'; + config.tlsCACerts.path = true; + call.should.throw(err); + }); + }); + + describe(prop('pem'), () => { + beforeEach(() => { + delete config.tlsCACerts.path; + }); + + it('should not throw when setting property instead of sibling "path" property', () => { + config.tlsCACerts.pem = 'asdf'; + call.should.not.throw(); + }); + + it('should throw for a non-string value', () => { + const err = 'child "tlsCACerts" fails because [child "pem" fails because ["pem" must be a string]]'; + config.tlsCACerts.pem = true; + call.should.throw(err); + }); + + it('should throw for an empty string value', () => { + const err = 'child "tlsCACerts" fails because [child "pem" fails because ["pem" is not allowed to be empty, "pem" length must be at least 1 characters long]]'; + config.tlsCACerts.pem = ''; + call.should.throw(err); + }); + }); + }); + }); + + describe('Function: validateOrganization', () => { + let config = { + mspid: 'Org1MSP', + peers: [ + 'peer0.org1.example.com', + 'peer1.org1.example.com' + ], + certificateAuthorities: [ + 'ca0.org1.example.com', + 'ca1.org1.example.com' + ], + adminPrivateKey: { + path: 'path' + }, + signedCert: { + path: 'path' + } + }; + let configString = JSON.stringify(config); + + beforeEach(() => { + config = JSON.parse(configString); + }); + + /** + * Wraps the actual call, so "should" can call this function without parameters + */ + function call() { + ConfigValidator.validateOrganization(config, + ['peer0.org1.example.com', 'peer1.org1.example.com'], + ['ca0.org1.example.com', 'ca1.org1.example.com']); + } + + it('should not throw for a valid value', () => { + call.should.not.throw(); + }); + + it('should throw for an unknown child property', () => { + const err = '"unknown" is not allowed'; + config.unknown = ''; + call.should.throw(err); + }); + + describe(prop('mspid'), () => { + it('should throw for missing required property', () => { + const err = 'child "mspid" fails because ["mspid" is required]'; + delete config.mspid; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "mspid" fails because ["mspid" must be a string]'; + config.mspid = true; + call.should.throw(err); + }); + + it('should throw for an empty string value', () => { + const err = 'child "mspid" fails because ["mspid" is not allowed to be empty, "mspid" length must be at least 1 characters long]'; + config.mspid = ''; + call.should.throw(err); + }); + }); + + describe(prop('peers'), () => { + it('should not throw for missing optional property', () => { + delete config.peers; + call.should.not.throw(); + }); + + it('should throw for a non-array value', () => { + const err = 'child "peers" fails because ["peers" must be an array]'; + config.peers = true; + call.should.throw(err); + }); + + it('should throw for an empty array value', () => { + const err = 'child "peers" fails because ["peers" must contain at least 1 items]'; + config.peers = []; + call.should.throw(err); + }); + + it('should throw for an undefined element', () => { + const err = 'child "peers" fails because ["peers" must not be a sparse array]'; + config.peers.push(undefined); + call.should.throw(err); + }); + + it('should throw for a duplicate reference', () => { + const err = 'child "peers" fails because ["peers" position 2 contains a duplicate value]'; + config.peers.push('peer0.org1.example.com'); + call.should.throw(err); + }); + + it('should throw for a non-existing reference', () => { + const err = 'child "peers" fails because ["peers" at position 2 fails because ["2" must be one of [peer0.org1.example.com, peer1.org1.example.com]]]'; + config.peers.push('peer3.org1.example.com'); + call.should.throw(err); + }); + }); + + describe(prop('certificateAuthorities'), () => { + it('should not throw for missing optional property', () => { + delete config.certificateAuthorities; + call.should.not.throw(); + }); + + it('should throw for a non-array value', () => { + const err = 'child "certificateAuthorities" fails because ["certificateAuthorities" must be an array]'; + config.certificateAuthorities = true; + call.should.throw(err); + }); + + it('should throw for an empty array value', () => { + const err = 'child "certificateAuthorities" fails because ["certificateAuthorities" must contain at least 1 items]'; + config.certificateAuthorities = []; + call.should.throw(err); + }); + + it('should throw for an undefined element', () => { + const err = 'child "certificateAuthorities" fails because ["certificateAuthorities" must not be a sparse array]'; + config.certificateAuthorities.push(undefined); + call.should.throw(err); + }); + + it('should throw for a duplicate reference', () => { + const err = 'child "certificateAuthorities" fails because ["certificateAuthorities" position 2 contains a duplicate value]'; + config.certificateAuthorities.push('ca0.org1.example.com'); + call.should.throw(err); + }); + + it('should throw for a non-existing reference', () => { + const err = 'child "certificateAuthorities" fails because ["certificateAuthorities" at position 2 fails because ["2" must be one of [ca0.org1.example.com, ca1.org1.example.com]]]'; + config.certificateAuthorities.push('ca5.org1.example.com'); + call.should.throw(err); + }); + }); + + describe(prop('adminPrivateKey'), () => { + it('should not throw for missing property when sibling "signedCert" not set either', () => { + delete config.adminPrivateKey; + delete config.signedCert; + call.should.not.throw(); + }); + + it('should throw for not setting together with sibling "signedCert" property', () => { + const err = '"value" contains [adminPrivateKey] without its required peers [signedCert]'; + delete config.signedCert; + call.should.throw(err); + }); + + it('should throw for a non-object value', () => { + const err = 'child "adminPrivateKey" fails because ["adminPrivateKey" must be an object]'; + config.adminPrivateKey = 'yes'; + call.should.throw(err); + }); + + it('should throw for an empty value', () => { + const err = 'child "adminPrivateKey" fails because ["value" must contain at least one of [pem, path]]'; + config.adminPrivateKey = {}; + call.should.throw(err); + }); + + it('should throw when setting both "path" and "pem" child properties', () => { + const err = 'child "adminPrivateKey" fails because ["value" contains a conflict between exclusive peers [pem, path]]'; + config.adminPrivateKey.pem = 'asdf'; + call.should.throw(err); + }); + + it('should throw for unknown child property', () => { + const err = 'child "adminPrivateKey" fails because ["unknown" is not allowed]'; + config.adminPrivateKey.unknown = ''; + call.should.throw(err); + }); + + describe(prop('path'), () => { + it('should throw for an empty string value', () => { + const err = 'child "adminPrivateKey" fails because [child "path" fails because ["path" is not allowed to be empty, "path" length must be at least 1 characters long]]'; + config.adminPrivateKey.path = ''; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "adminPrivateKey" fails because [child "path" fails because ["path" must be a string]]'; + config.adminPrivateKey.path = true; + call.should.throw(err); + }); + }); + + describe(prop('pem'), () => { + beforeEach(() => { + delete config.adminPrivateKey.path; + }); + + it('should not throw when setting property instead of sibling "path" property', () => { + config.adminPrivateKey.pem = 'asdf'; + call.should.not.throw(); + }); + + it('should throw for a non-string value', () => { + const err = 'child "adminPrivateKey" fails because [child "pem" fails because ["pem" must be a string]]'; + config.adminPrivateKey.pem = true; + call.should.throw(err); + }); + + it('should throw for an empty string value', () => { + const err = 'child "adminPrivateKey" fails because [child "pem" fails because ["pem" is not allowed to be empty, "pem" length must be at least 1 characters long]]'; + config.adminPrivateKey.pem = ''; + call.should.throw(err); + }); + }); + }); + + describe(prop('signedCert'), () => { + it('should throw for not setting together with sibling "adminPrivateKey" property', () => { + const err = '"value" contains [signedCert] without its required peers [adminPrivateKey]'; + delete config.adminPrivateKey; + call.should.throw(err); + }); + + it('should throw for a non-object value', () => { + const err = 'child "signedCert" fails because ["signedCert" must be an object]'; + config.signedCert = 'yes'; + call.should.throw(err); + }); + + it('should throw for an empty value', () => { + const err = 'child "signedCert" fails because ["value" must contain at least one of [pem, path]]'; + config.signedCert = {}; + call.should.throw(err); + }); + + it('should throw when setting both "path" and "pem" child properties', () => { + const err = 'child "signedCert" fails because ["value" contains a conflict between exclusive peers [pem, path]]'; + config.signedCert.pem = 'asdf'; + call.should.throw(err); + }); + + it('should throw for unknown child property', () => { + const err = 'child "signedCert" fails because ["unknown" is not allowed]'; + config.signedCert.unknown = ''; + call.should.throw(err); + }); + + describe(prop('path'), () => { + it('should throw for an empty string value', () => { + const err = 'child "signedCert" fails because [child "path" fails because ["path" is not allowed to be empty, "path" length must be at least 1 characters long]]'; + config.signedCert.path = ''; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "signedCert" fails because [child "path" fails because ["path" must be a string]]'; + config.signedCert.path = true; + call.should.throw(err); + }); + }); + + describe(prop('pem'), () => { + beforeEach(() => { + delete config.signedCert.path; + }); + + it('should not throw when setting property instead of sibling "path" property', () => { + config.signedCert.pem = 'asdf'; + call.should.not.throw(); + }); + + it('should throw for a non-string value', () => { + const err = 'child "signedCert" fails because [child "pem" fails because ["pem" must be a string]]'; + config.signedCert.pem = true; + call.should.throw(err); + }); + + it('should throw for an empty string value', () => { + const err = 'child "signedCert" fails because [child "pem" fails because ["pem" is not allowed to be empty, "pem" length must be at least 1 characters long]]'; + config.signedCert.pem = ''; + call.should.throw(err); + }); + }); + }); + }); + + describe('Function: validateClient', () => { + let config = { + client: { + organization: 'Org1', + credentialStore: { + path: 'path', + cryptoStore: { + path: 'path' + } + }, + + clientPrivateKey: { + path: 'path' + }, + clientSignedCert: { + path: 'path' + }, + connection: { + timeout: { + peer: { + endorser: 120, + eventHub: 60, + eventReg: 3 + }, + orderer: 30 + }, + } + // other properties are added during the tests + } + }; + let configString = JSON.stringify(config); + + let wallet = false; + + // reset the config before every test + beforeEach(() => { + config = JSON.parse(configString); + wallet = false; + }); + + /** + * Wraps the actual call, so "should" can call this function without parameters + */ + function call() { + ConfigValidator.validateClient(config, + ['Org1', 'Org2'], wallet); + } + + it('should not throw for a valid value', () => { + call.should.not.throw(); + }); + + describe(prop('client'), () => { + it('should throw for missing property', () => { + const err = 'child "client" fails because ["client" is required]'; + delete config.client; + call.should.throw(err); + }); + + it('should throw for a non-object value', () => { + const err = 'child "client" fails because ["client" must be an object]'; + config.client = true; + call.should.throw(err); + }); + + it('should throw for unknown child property', () => { + const err = '"unknown" is not allowed'; + config.unknown = 'invalid'; + call.should.throw(err); + }); + + it('should throw if "clientSignedCert" is set without "clientPrivateKey"', () => { + const err = 'child "client" fails because ["value" contains [clientSignedCert] without its required peers [clientPrivateKey]]'; + delete config.client.clientPrivateKey; + call.should.throw(err); + }); + + it('should throw if "affiliation" is set together with client materials', () => { + const err = 'child "client" fails because ["value" contains a conflict between exclusive peers [affiliation, enrollmentSecret, clientSignedCert]]'; + config.client.affiliation = 'aff'; + call.should.throw(err); + }); + + it('should throw if "enrollmentSecret" is set together with client materials', () => { + const err = 'child "client" fails because ["value" contains a conflict between exclusive peers [affiliation, enrollmentSecret, clientSignedCert]]'; + config.client.enrollmentSecret = 'secret'; + call.should.throw(err); + }); + + it('should throw if "enrollmentSecret" is set together with "affiliation"', () => { + const err = 'child "client" fails because ["value" contains a conflict between exclusive peers [affiliation, enrollmentSecret, clientSignedCert]]'; + delete config.client.clientPrivateKey; + delete config.client.clientSignedCert; + config.client.affiliation = 'aff'; + config.client.enrollmentSecret = 'secret'; + call.should.throw(err); + }); + + it('should throw if no credential options are set when not using a wallet', () => { + const err = 'child "client" fails because ["value" must contain at least one of [affiliation, enrollmentSecret, clientSignedCert]]'; + delete config.client.clientPrivateKey; + delete config.client.clientSignedCert; + call.should.throw(err); + }); + + it('should not throw if no credential options are set when using a wallet', () => { + wallet = true; + delete config.client.clientPrivateKey; + delete config.client.clientSignedCert; + delete config.client.credentialStore; + call.should.not.throw(); + }); + + describe(prop('organization'), () => { + it('should throw for missing required property', () => { + const err = 'child "client" fails because [child "organization" fails because ["organization" is required]]'; + delete config.client.organization; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "client" fails because [child "organization" fails because ["organization" must be a string]]'; + config.client.organization = true; + call.should.throw(err); + }); + + it('should throw for a non-existing reference', () => { + const err = 'child "client" fails because [child "organization" fails because ["organization" must be one of [Org1, Org2]]]'; + config.client.organization = 'Org5'; + call.should.throw(err); + }); + }); + + describe(prop('credentialStore'), () => { + it('should throw for missing required property', () => { + const err = 'child "client" fails because [child "credentialStore" fails because ["credentialStore" is required]]'; + delete config.client.credentialStore; + call.should.throw(err); + }); + + it('should throw for a non-object value', () => { + const err = 'child "client" fails because [child "credentialStore" fails because ["credentialStore" must be an object]]'; + config.client.credentialStore = true; + call.should.throw(err); + }); + + it('should throw for property when using a wallet', () => { + const err = 'child "client" fails because [child "credentialStore" fails because ["credentialStore" is not allowed]]'; + wallet = true; + delete config.client.clientPrivateKey; + delete config.client.clientSignedCert; + call.should.throw(err); + }); + + it('should throw for unknown child property', () => { + const err = 'child "client" fails because [child "credentialStore" fails because ["unknown" is not allowed]]'; + config.client.credentialStore.unknown = 'invalid'; + call.should.throw(err); + }); + + describe(prop('path'), () => { + it('should throw for missing required property', () => { + const err = 'child "client" fails because [child "credentialStore" fails because [child "path" fails because ["path" is required]]]'; + delete config.client.credentialStore.path; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "client" fails because [child "credentialStore" fails because [child "path" fails because ["path" must be a string]]]'; + config.client.credentialStore.path = true; + call.should.throw(err); + }); + + it('should throw for an empty string value', () => { + const err = 'child "client" fails because [child "credentialStore" fails because [child "path" fails because ["path" is not allowed to be empty, "path" length must be at least 1 characters long]]]'; + config.client.credentialStore.path = ''; + call.should.throw(err); + }); + }); + + describe(prop('cryptoStore'), () => { + it('should throw for missing required property', () => { + const err = 'child "client" fails because [child "credentialStore" fails because [child "cryptoStore" fails because ["cryptoStore" is required]]]'; + delete config.client.credentialStore.cryptoStore; + call.should.throw(err); + }); + + it('should throw for a non-object value', () => { + const err = 'child "client" fails because [child "credentialStore" fails because [child "cryptoStore" fails because ["cryptoStore" must be an object]]]'; + config.client.credentialStore.cryptoStore = true; + call.should.throw(err); + }); + + describe(prop('path'), () => { + it('should throw for missing required property', () => { + const err = 'child "client" fails because [child "credentialStore" fails because [child "cryptoStore" fails because [child "path" fails because ["path" is required]]]]'; + delete config.client.credentialStore.cryptoStore.path; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "client" fails because [child "credentialStore" fails because [child "cryptoStore" fails because [child "path" fails because ["path" must be a string]]]]'; + config.client.credentialStore.cryptoStore.path = true; + call.should.throw(err); + }); + + it('should throw for an empty string value', () => { + const err = 'child "client" fails because [child "credentialStore" fails because [child "cryptoStore" fails because [child "path" fails because ["path" is not allowed to be empty, "path" length must be at least 1 characters long]]]]'; + config.client.credentialStore.cryptoStore.path = ''; + call.should.throw(err); + }); + }); + }); + }); + + describe(prop('clientPrivateKey'), () => { + it('should throw for property when using a wallet', () => { + const err = 'child "client" fails because [child "clientPrivateKey" fails because ["clientPrivateKey" is not allowed]]'; + wallet = true; + delete config.client.credentialStore; + delete config.client.clientSignedCert; + call.should.throw(err); + }); + + it('should throw for a non-object value', () => { + const err = 'child "client" fails because [child "clientPrivateKey" fails because ["clientPrivateKey" must be an object]]'; + config.client.clientPrivateKey = 'yes'; + call.should.throw(err); + }); + + it('should throw for an empty value', () => { + const err = 'child "client" fails because [child "clientPrivateKey" fails because ["value" must contain at least one of [pem, path]]]'; + config.client.clientPrivateKey = {}; + call.should.throw(err); + }); + + it('should throw when setting both "path" and "pem" child properties', () => { + const err = 'child "client" fails because [child "clientPrivateKey" fails because ["value" contains a conflict between exclusive peers [pem, path]]]'; + config.client.clientPrivateKey.pem = 'asdf'; + call.should.throw(err); + }); + + it('should throw for unknown child property', () => { + const err = 'child "client" fails because [child "clientPrivateKey" fails because ["unknown" is not allowed]]'; + config.client.clientPrivateKey.unknown = ''; + call.should.throw(err); + }); + + describe(prop('path'), () => { + it('should throw for an empty string value', () => { + const err = 'child "client" fails because [child "clientPrivateKey" fails because [child "path" fails because ["path" is not allowed to be empty, "path" length must be at least 1 characters long]]]'; + config.client.clientPrivateKey.path = ''; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "client" fails because [child "clientPrivateKey" fails because [child "path" fails because ["path" must be a string]]]'; + config.client.clientPrivateKey.path = true; + call.should.throw(err); + }); + }); + + describe(prop('pem'), () => { + beforeEach(() => { + delete config.client.clientPrivateKey.path; + }); + + it('should not throw when setting property instead of sibling "path" property', () => { + config.client.clientPrivateKey.pem = 'asdf'; + call.should.not.throw(); + }); + + it('should throw for a non-string value', () => { + const err = 'child "client" fails because [child "clientPrivateKey" fails because [child "pem" fails because ["pem" must be a string]]]'; + config.client.clientPrivateKey.pem = true; + call.should.throw(err); + }); + + it('should throw for an empty string value', () => { + const err = 'child "client" fails because [child "clientPrivateKey" fails because [child "pem" fails because ["pem" is not allowed to be empty, "pem" length must be at least 1 characters long]]]'; + config.client.clientPrivateKey.pem = ''; + call.should.throw(err); + }); + }); + }); + + describe(prop('clientSignedCert'), () => { + it('should throw for property when using a wallet', () => { + const err = 'child "client" fails because [child "clientSignedCert" fails because ["clientSignedCert" is not allowed]]'; + wallet = true; + delete config.client.credentialStore; + delete config.client.clientPrivateKey; + call.should.throw(err); + }); + + it('should throw for a non-object value', () => { + const err = 'child "client" fails because [child "clientSignedCert" fails because ["clientSignedCert" must be an object]]'; + config.client.clientSignedCert = 'yes'; + call.should.throw(err); + }); + + it('should throw for an empty value', () => { + const err = 'child "client" fails because [child "clientSignedCert" fails because ["value" must contain at least one of [pem, path]]]'; + config.client.clientSignedCert = {}; + call.should.throw(err); + }); + + it('should throw when setting both "path" and "pem" child properties', () => { + const err = 'child "client" fails because [child "clientSignedCert" fails because ["value" contains a conflict between exclusive peers [pem, path]]]'; + config.client.clientSignedCert.pem = 'asdf'; + call.should.throw(err); + }); + + it('should throw for unknown child property', () => { + const err = 'child "client" fails because [child "clientSignedCert" fails because ["unknown" is not allowed]]'; + config.client.clientSignedCert.unknown = ''; + call.should.throw(err); + }); + + describe(prop('path'), () => { + it('should throw for an empty string value', () => { + const err = 'child "client" fails because [child "clientSignedCert" fails because [child "path" fails because ["path" is not allowed to be empty, "path" length must be at least 1 characters long]]]'; + config.client.clientSignedCert.path = ''; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "client" fails because [child "clientSignedCert" fails because [child "path" fails because ["path" must be a string]]]'; + config.client.clientSignedCert.path = true; + call.should.throw(err); + }); + }); + + describe(prop('pem'), () => { + beforeEach(() => { + delete config.client.clientSignedCert.path; + }); + + it('should not throw when setting property instead of sibling "path" property', () => { + config.client.clientSignedCert.pem = 'asdf'; + call.should.not.throw(); + }); + + it('should throw for a non-string value', () => { + const err = 'child "client" fails because [child "clientSignedCert" fails because [child "pem" fails because ["pem" must be a string]]]'; + config.client.clientSignedCert.pem = true; + call.should.throw(err); + }); + + it('should throw for an empty string value', () => { + const err = 'child "client" fails because [child "clientSignedCert" fails because [child "pem" fails because ["pem" is not allowed to be empty, "pem" length must be at least 1 characters long]]]'; + config.client.clientSignedCert.pem = ''; + call.should.throw(err); + }); + }); + }); + + describe(prop('connection'), () => { + it('should not throw for missing optional property', () => { + delete config.client.connection; + call.should.not.throw(); + }); + + it('should throw for a non-object value', () => { + const err = 'child "client" fails because [child "connection" fails because ["connection" must be an object]]'; + config.client.connection = true; + call.should.throw(err); + }); + + it('should throw for an unknown child property', () => { + const err = 'child "client" fails because [child "connection" fails because ["unknown" is not allowed]]'; + config.client.connection.unknown = ''; + call.should.throw(err); + }); + + describe(prop('timeout'), () => { + it('should throw for missing property', () => { + const err = 'child "client" fails because [child "connection" fails because [child "timeout" fails because ["timeout" is required]]]'; + delete config.client.connection.timeout; + call.should.throw(err); + }); + + it('should throw for a non-object value', () => { + const err = 'child "client" fails because [child "connection" fails because [child "timeout" fails because ["timeout" must be an object]]]'; + config.client.connection.timeout = true; + call.should.throw(err); + }); + + it('should throw for an empty value', () => { + const err = 'child "client" fails because [child "connection" fails because [child "timeout" fails because ["value" must contain at least one of [peer, orderer]]]]'; + config.client.connection.timeout = {}; + call.should.throw(err); + }); + + it('should throw for an unknown child property', () => { + const err = 'child "client" fails because [child "connection" fails because [child "timeout" fails because ["unknown" is not allowed]]]'; + config.client.connection.timeout.unknown = ''; + call.should.throw(err); + }); + + describe(prop('peer'), () => { + it('should not throw for missing optional property', () => { + delete config.client.connection.timeout.peer; + call.should.not.throw(); + }); + + it('should throw for a non-object value', () => { + const err = 'child "client" fails because [child "connection" fails because [child "timeout" fails because [child "peer" fails because ["peer" must be an object]]]]'; + config.client.connection.timeout.peer = true; + call.should.throw(err); + }); + + it('should throw for an empty value', () => { + const err = 'child "client" fails because [child "connection" fails because [child "timeout" fails because [child "peer" fails because ["value" must contain at least one of [endorser, eventHub, eventReg]]]]]'; + config.client.connection.timeout.peer = {}; + call.should.throw(err); + }); + + it('should throw for an unknown child property', () => { + const err = 'child "client" fails because [child "connection" fails because [child "timeout" fails because [child "peer" fails because ["unknown" is not allowed]]]]'; + config.client.connection.timeout.peer.unknown = ''; + call.should.throw(err); + }); + + describe(prop('endorser'), () => { + it('should not throw for missing optional property', () => { + delete config.client.connection.timeout.peer.endorser; + call.should.not.throw(); + }); + + it('should throw for a non-number value', () => { + const err = 'child "client" fails because [child "connection" fails because [child "timeout" fails because [child "peer" fails because [child "endorser" fails because ["endorser" must be a number]]]]]'; + config.client.connection.timeout.peer.endorser = true; + call.should.throw(err); + }); + + it('should throw for a negative value', () => { + const err = 'child "client" fails because [child "connection" fails because [child "timeout" fails because [child "peer" fails because [child "endorser" fails because ["endorser" must be a positive number]]]]]'; + config.client.connection.timeout.peer.endorser = -10; + call.should.throw(err); + }); + }); + + describe(prop('eventHub'), () => { + it('should not throw for missing optional property', () => { + delete config.client.connection.timeout.peer.eventHub; + call.should.not.throw(); + }); + + it('should throw for a non-number value', () => { + const err = 'child "client" fails because [child "connection" fails because [child "timeout" fails because [child "peer" fails because [child "eventHub" fails because ["eventHub" must be a number]]]]]'; + config.client.connection.timeout.peer.eventHub = true; + call.should.throw(err); + }); + + it('should throw for a negative value', () => { + const err = 'child "client" fails because [child "connection" fails because [child "timeout" fails because [child "peer" fails because [child "eventHub" fails because ["eventHub" must be a positive number]]]]]'; + config.client.connection.timeout.peer.eventHub = -10; + call.should.throw(err); + }); + }); + + describe(prop('eventReg'), () => { + it('should not throw for missing optional property', () => { + delete config.client.connection.timeout.peer.eventReg; + call.should.not.throw(); + }); + + it('should throw for a non-number value', () => { + const err = 'child "client" fails because [child "connection" fails because [child "timeout" fails because [child "peer" fails because [child "eventReg" fails because ["eventReg" must be a number]]]]]'; + config.client.connection.timeout.peer.eventReg = true; + call.should.throw(err); + }); + + it('should throw for a negative value', () => { + const err = 'child "client" fails because [child "connection" fails because [child "timeout" fails because [child "peer" fails because [child "eventReg" fails because ["eventReg" must be a positive number]]]]]'; + config.client.connection.timeout.peer.eventReg = -10; + call.should.throw(err); + }); + }); + }); + + describe(prop('orderer'), () => { + it('should not throw for missing optional property', () => { + delete config.client.connection.timeout.orderer; + call.should.not.throw(); + }); + + it('should throw for a non-number value', () => { + const err = 'child "client" fails because [child "connection" fails because [child "timeout" fails because [child "orderer" fails because ["orderer" must be a number]]]]'; + config.client.connection.timeout.orderer = true; + call.should.throw(err); + }); + + it('should throw for a negative value', () => { + const err = 'child "client" fails because [child "connection" fails because [child "timeout" fails because [child "orderer" fails because ["orderer" must be a positive number]]]]'; + config.client.connection.timeout.orderer = -10; + call.should.throw(err); + }); + }); + }); + }); + + describe(prop('affiliation'), () => { + beforeEach(() => { + delete config.client.clientPrivateKey; + delete config.client.clientSignedCert; + }); + + it('should throw for property when using a wallet', () => { + const err = 'child "client" fails because [child "affiliation" fails because ["affiliation" is not allowed]]'; + wallet = true; + delete config.client.credentialStore; + config.client.affiliation = 'aff'; + call.should.throw(err); + }); + + it('should not throw for setting it without client materials', () => { + config.client.affiliation = 'aff'; + call.should.not.throw(); + }); + + it('should throw for a non-string value', () => { + const err = 'child "client" fails because [child "affiliation" fails because ["affiliation" must be a string]]'; + config.client.affiliation = true; + call.should.throw(err); + }); + + it('should throw for an empty string', () => { + const err = 'child "client" fails because [child "affiliation" fails because ["affiliation" is not allowed to be empty, "affiliation" length must be at least 1 characters long]]'; + config.client.affiliation = ''; + call.should.throw(err); + }); + }); + + describe(prop('attributes'), () => { + beforeEach(() => { + delete config.client.clientPrivateKey; + delete config.client.clientSignedCert; + config.client.affiliation = 'aff'; + }); + + it('should throw for property when using a wallet', () => { + const err = 'child "client" fails because [child "affiliation" fails because ["affiliation" is not allowed]]'; + wallet = true; + delete config.client.credentialStore; + call.should.throw(err); + }); + + it('should not throw for setting it without client materials but with affiliation', () => { + config.client.attributes = [ {name: 'attr1', value: 'val1', ecert: true}, {name: 'attr2', value: 'val2'}]; + call.should.not.throw(); + }); + + it('should throw if set without affiliation', () => { + const err = 'child "client" fails because ["attributes" missing required peer "affiliation", "value" must contain at least one of [affiliation, enrollmentSecret, clientSignedCert]]'; + delete config.client.affiliation; + config.client.attributes = [ {name: 'attr1', value: 'val1', ecert: true}, {name: 'attr2', value: 'val2'}]; + call.should.throw(err); + }); + + it('should throw for a non-array value', () => { + const err = 'child "client" fails because [child "attributes" fails because ["attributes" must be an array]]'; + config.client.attributes = true; + call.should.throw(err); + }); + + it('should throw for an empty value', () => { + const err = 'child "client" fails because [child "attributes" fails because ["attributes" must contain at least 1 items]]'; + config.client.attributes = []; + call.should.throw(err); + }); + + it('should throw for an undefined value', () => { + const err = 'child "client" fails because [child "attributes" fails because ["attributes" must not be a sparse array]]'; + config.client.attributes = [undefined]; + call.should.throw(err); + }); + + it('should throw for an unknown child property of an item', () => { + const err = 'child "client" fails because [child "attributes" fails because ["attributes" at position 1 fails because ["unknown" is not allowed]]]'; + config.client.attributes = [ {name: 'attr1', value: 'val1', ecert: true}, {name: 'attr2', value: 'val2', unknown: ''}]; + call.should.throw(err); + }); + + describe(prop('[item].name'), () => { + it('should throw for duplicate names', () => { + const err = 'child "client" fails because [child "attributes" fails because ["attributes" position 1 contains a duplicate value]]'; + config.client.attributes = [ {name: 'attr1', value: 'val1', ecert: true}, {name: 'attr1', value: 'val2'}]; + call.should.throw(err); + }); + + it('should throw for name with empty string', () => { + const err = 'child "client" fails because [child "attributes" fails because ["attributes" at position 1 fails because [child "name" fails because ["name" is not allowed to be empty, "name" length must be at least 1 characters long]]]]'; + config.client.attributes = [ {name: 'attr1', value: 'val1', ecert: true}, {name: '', value: 'val2'}]; + call.should.throw(err); + }); + + it('should throw for missing name', () => { + const err = 'child "client" fails because [child "attributes" fails because ["attributes" at position 0 fails because [child "name" fails because ["name" is required]]]]'; + config.client.attributes = [ {value: 'val1', ecert: true}, {name: 'attr1', value: 'val2'}]; + call.should.throw(err); + }); + }); + + describe(prop('[item].value'), () => { + it('should throw for missing value', () => { + const err = 'child "client" fails because [child "attributes" fails because ["attributes" at position 0 fails because [child "value" fails because ["value" is required]]]]'; + config.client.attributes = [ {name: 'attr1', ecert: true}, {name: 'attr2', value: 'val2'}]; + call.should.throw(err); + }); + }); + + describe(prop('[item].ecert'), () => { + it('should throw for non-boolean value', () => { + const err = 'child "client" fails because [child "attributes" fails because ["attributes" at position 0 fails because [child "ecert" fails because ["ecert" must be a boolean]]]]'; + config.client.attributes = [ {name: 'attr1', value: 'val1', ecert: 'ecert'}, {name: 'attr2', value: 'val2'}]; + call.should.throw(err); + }); + }); + }); + + describe(prop('enrollmentSecret'), () => { + beforeEach(() => { + delete config.client.clientPrivateKey; + delete config.client.clientSignedCert; + }); + + it('should throw for property when using a wallet', () => { + const err = 'child "client" fails because [child "enrollmentSecret" fails because ["enrollmentSecret" is not allowed]]'; + wallet = true; + delete config.client.credentialStore; + config.client.enrollmentSecret = 'secret'; + call.should.throw(err); + }); + + it('should not throw for if set without client materials', () => { + config.client.enrollmentSecret = 'secret'; + call.should.not.throw(); + }); + + it('should throw for a non-string value', () => { + const err = 'child "client" fails because [child "enrollmentSecret" fails because ["enrollmentSecret" must be a string]]'; + config.client.enrollmentSecret = true; + call.should.throw(err); + }); + + it('should throw for an empty string value', () => { + const err = 'child "client" fails because [child "enrollmentSecret" fails because ["enrollmentSecret" is not allowed to be empty, "enrollmentSecret" length must be at least 1 characters long]]'; + config.client.enrollmentSecret = ''; + call.should.throw(err); + }); + }); + }); + }); + + describe('Function: validateChannel', () => { + let config = { + created: false, + configBinary: 'path', + orderers: ['orderer.example.com'], + peers: { + 'peer0.org1.example.com': { + eventSource: true, + endorsingPeer: true, + chaincodeQuery: true, + ledgerQuery: true + }, + 'peer0.org2.example.com': { + eventSource: true, + endorsingPeer: true, + chaincodeQuery: true, + ledgerQuery: true + } + }, + chaincodes: [ + { + id: 'marbles', + contractID: 'ContractMarbles', + version: 'v0', + language: 'golang', + path: 'path', + metadataPath: 'path', + targetPeers: ['peer0.org1.example.com', 'peer0.org2.example.com'], + 'endorsement-policy': { + identities: [ + { role: { name: 'member', mspId: 'Org1MSP' }}, + { role: { name: 'member', mspId: 'Org2MSP' }} + ], + policy: { + '2-of': [ + { 'signed-by': 1}, + { '1-of': [{ 'signed-by': 0 }, { 'signed-by': 1 }]} + ] + } + }, + init: [], + function: 'init', + initTransientMap: { + key: 'value' + }, + 'collections-config': [ + { + name: 'name', + policy: { + identities: [ + {role: {name: 'member', mspId: 'Org1MSP'}}, + {role: {name: 'member', mspId: 'Org2MSP'}} + ], + policy: { + '2-of': [ + {'signed-by': 1}, + {'1-of': [{'signed-by': 0}, {'signed-by': 1}]} + ] + } + }, + requiredPeerCount: 1, + maxPeerCount: 2, + blockToLive: 0 + }, + { + name: 'name2', + policy: { + identities: [ + {role: {name: 'member', mspId: 'Org1MSP'}}, + {role: {name: 'member', mspId: 'Org2MSP'}} + ], + policy: { + '2-of': [ + {'signed-by': 1}, + {'1-of': [{'signed-by': 0}, {'signed-by': 1}]} + ] + } + }, + requiredPeerCount: 1, + maxPeerCount: 2, + blockToLive: 0 + } + ] + }, + { + id: 'drm', + version: 'v0' + } + ] + + // additional properties added by the tests + }; + let configString = JSON.stringify(config); + + beforeEach(() => { + config = JSON.parse(configString); + }); + + /** + * Wraps the actual call, so "should" can call this function without parameters + */ + function call() { + ConfigValidator.validateChannel(config, + ['orderer.example.com'], + ['peer0.org1.example.com', 'peer0.org2.example.com'], + ['Org1MSP', 'Org2MSP'], + ['Contract1'], + flowOptions, + discovery); + } + + it('should not throw for a valid value', () => { + call.should.not.throw(); + }); + + it('should throw for unknown child property', () => { + const err = '"unknown" is not allowed'; + config.unknown = ''; + call.should.throw(err); + }); + + it('should throw when both "configBinary" and "definition" is set for created channel', () => { + const err = '"value" contains a conflict between optional exclusive peers [configBinary, definition]'; + config.created = true; + config.definition = { + capabilities : [], + consortium : 'SampleConsortium', + msps : [ 'Org1MSP', 'Org2MSP' ], + version : 0 + }; + call.should.throw(err); + }); + + describe(prop('created'), () => { + it('should not throw for missing optional property', () => { + delete config.created; + call.should.not.throw(); + }); + + it('should throw for a non-boolean value', () => { + const err = 'child "created" fails because ["created" must be a boolean]'; + config.created = 'yes'; + call.should.throw(err); + }); + }); + + describe(prop('configBinary'), () => { + it('should throw for an empty string value', () => { + const err = 'child "configBinary" fails because ["configBinary" is not allowed to be empty, "configBinary" length must be at least 1 characters long]'; + config.configBinary = ''; + call.should.throw(err); + }); + + it('should throw for a non-string value', () => { + const err = 'child "configBinary" fails because ["configBinary" must be a string]'; + config.configBinary = true; + call.should.throw(err); + }); + + it('should not throw for missing property when channel is created', () => { + config.created = true; + delete config.configBinary; + call.should.not.throw(); + }); + + it('should not throw for missing property when definition is set', () => { + delete config.configBinary; + config.definition = { + capabilities : [], + consortium : 'SampleConsortium', + msps : [ 'Org1MSP', 'Org2MSP' ], + version : 0 + }; + call.should.not.throw(); + }); + + it('should throw for property when definition is set', () => { + const err = 'child "configBinary" fails because ["configBinary" is not allowed]'; + config.definition = { + capabilities : [], + consortium : 'SampleConsortium', + msps : [ 'Org1MSP', 'Org2MSP' ], + version : 0 + }; + call.should.throw(err); + }); + }); + + describe(prop('definition'), () => { + beforeEach(() => { + delete config.configBinary; + config.definition = { + capabilities : [], + consortium : 'SampleConsortium', + msps : [ 'Org1MSP', 'Org2MSP' ], + version : 0 + }; + }); + + it('should not throw for setting it instead of "configBinary"', () => { + call.should.not.throw(); + }); + + it('should not throw for missing property when channel is created', () => { + config.created = true; + delete config.definition; + call.should.not.throw(); + }); + + it('should throw for non-object value', () => { + const err = 'child "definition" fails because ["definition" must be an object]'; + config.definition = true; + call.should.throw(err); + }); + + it('should throw for empty object', () => { + const err = 'child "definition" fails because [child "capabilities" fails because ["capabilities" is required], child "consortium" fails because ["consortium" is required], child "msps" fails because ["msps" is required], child "version" fails because ["version" is required]]'; + config.definition = {}; + call.should.throw(err); + }); + + describe(prop('capabilities'), () => { + it('should throw for missing required property', () => { + const err = 'child "definition" fails because [child "capabilities" fails because ["capabilities" is required]]'; + delete config.definition.capabilities; + call.should.throw(err); + }); + + it('should throw for non-array value', () => { + const err = 'child "definition" fails because [child "capabilities" fails because ["capabilities" must be an array]]'; + config.definition.capabilities = true; + call.should.throw(err); + }); + + it('should throw for undefined elements', () => { + const err = 'child "definition" fails because [child "capabilities" fails because ["capabilities" must not be a sparse array]]'; + config.definition.capabilities = [ undefined ]; + call.should.throw(err); + }); + }); + + describe(prop('consortium'), () => { + it('should throw for missing required property', () => { + const err = 'child "definition" fails because [child "consortium" fails because ["consortium" is required]]'; + delete config.definition.consortium; + call.should.throw(err); + }); + + it('should throw for non-string value', () => { + const err = 'child "definition" fails because [child "consortium" fails because ["consortium" must be a string]]'; + config.definition.consortium = true; + call.should.throw(err); + }); + + it('should throw for an empty string value', () => { + const err = 'child "definition" fails because [child "consortium" fails because ["consortium" is not allowed to be empty, "consortium" length must be at least 1 characters long]]'; + config.definition.consortium = ''; + call.should.throw(err); + }); + }); + + describe(prop('msps'), () => { + it('should throw for missing required property', () => { + const err = 'child "definition" fails because [child "msps" fails because ["msps" is required]]'; + delete config.definition.msps; + call.should.throw(err); + }); + + it('should throw for non-array value', () => { + const err = 'child "definition" fails because [child "msps" fails because ["msps" must be an array]]'; + config.definition.msps = true; + call.should.throw(err); + }); + + it('should throw for an undefined item', () => { + const err = 'child "definition" fails because [child "msps" fails because ["msps" must not be a sparse array]]'; + config.definition.msps.push(undefined); + call.should.throw(err); + }); + + it('should throw for an invalid item', () => { + const err = 'child "definition" fails because [child "msps" fails because ["msps" at position 2 fails because ["2" must be one of [Org1MSP, Org2MSP]]]]'; + config.definition.msps.push('Org5MSP'); + call.should.throw(err); + }); + + it('should throw for duplicated items', () => { + const err = 'child "definition" fails because [child "msps" fails because ["msps" position 2 contains a duplicate value]]'; + config.definition.msps.push('Org1MSP'); + call.should.throw(err); + }); + }); + + describe(prop('version'), () => { + it('should throw for missing required property', () => { + const err = 'child "definition" fails because [child "version" fails because ["version" is required]]'; + delete config.definition.version; + call.should.throw(err); + }); + + it('should throw for non-integer value', () => { + const err = 'child "definition" fails because [child "version" fails because ["version" must be an integer]]'; + config.definition.version = 3.14; + call.should.throw(err); + }); + + it('should throw for a negative value', () => { + const err = 'child "definition" fails because [child "version" fails because ["version" must be larger than or equal to 0]]'; + config.definition.version = -10; + call.should.throw(err); + }); + }); + }); + + describe(prop('orderers'), () => { + it('should throw for missing required property', () => { + const err = 'child "orderers" fails because ["orderers" is required]'; + delete config.orderers; + call.should.throw(err); + }); + + it('should not throw for missing optional property in discovery mode', () => { + discovery = true; + delete config.orderers; + call.should.not.throw(); + }); + + it('should throw for non-array value', () => { + const err = 'child "orderers" fails because ["orderers" must be an array]'; + config.orderers = true; + call.should.throw(err); + }); + + it('should throw for an undefined item', () => { + const err = 'child "orderers" fails because ["orderers" must not be a sparse array]'; + config.orderers.push(undefined); + call.should.throw(err); + }); + + it('should throw for an invalid item', () => { + const err = 'child "orderers" fails because ["orderers" at position 1 fails because ["1" must be one of [orderer.example.com]]]'; + config.orderers.push('orderer2.example.com'); + call.should.throw(err); + }); + + it('should throw for duplicated items', () => { + const err = 'child "orderers" fails because ["orderers" position 1 contains a duplicate value]'; + config.orderers.push('orderer.example.com'); + call.should.throw(err); + }); + }); + + describe(prop('peers'), () => { + it('should throw for missing required property', () => { + const err = 'child "peers" fails because ["peers" is required]'; + delete config.peers; + call.should.throw(err); + }); + + it('should throw for non-object value', () => { + const err = 'child "peers" fails because ["peers" must be an object]'; + config.peers = true; + call.should.throw(err); + }); + + it('should throw for invalid child property, even if its structure would be valid', () => { + const err = 'child "peers" fails because ["unknown" is not allowed]'; + config.peers.unknown = {}; + call.should.throw(err); + }); + + describe(prop('[child].endorsingPeer'), () => { + it('should not throw for missing optional property', () => { + delete config.peers['peer0.org1.example.com'].endorsingPeer; + call.should.not.throw(); + }); + + it('should throw for non-bool value', () => { + const err = 'child "peers" fails because [child "peer0.org1.example.com" fails because [child "endorsingPeer" fails because ["endorsingPeer" must be a boolean]]]'; + config.peers['peer0.org1.example.com'].endorsingPeer = ''; + call.should.throw(err); + }); + }); + + describe(prop('[child].chaincodeQuery'), () => { + it('should not throw for missing optional property', () => { + delete config.peers['peer0.org1.example.com'].chaincodeQuery; + call.should.not.throw(); + }); + + it('should throw for non-bool value', () => { + const err = 'child "peers" fails because [child "peer0.org1.example.com" fails because [child "chaincodeQuery" fails because ["chaincodeQuery" must be a boolean]]]'; + config.peers['peer0.org1.example.com'].chaincodeQuery = ''; + call.should.throw(err); + }); + }); + + describe(prop('[child].ledgerQuery'), () => { + it('should not throw for missing optional property', () => { + delete config.peers['peer0.org1.example.com'].ledgerQuery; + call.should.not.throw(); + }); + + it('should throw for non-bool value', () => { + const err = 'child "peers" fails because [child "peer0.org1.example.com" fails because [child "ledgerQuery" fails because ["ledgerQuery" must be a boolean]]]'; + config.peers['peer0.org1.example.com'].ledgerQuery = ''; + call.should.throw(err); + }); + }); + + describe(prop('[child].eventSource'), () => { + it('should not throw for missing optional property', () => { + delete config.peers['peer0.org1.example.com'].eventSource; + call.should.not.throw(); + }); + + it('should throw for non-bool value', () => { + const err = 'child "peers" fails because [child "peer0.org1.example.com" fails because [child "eventSource" fails because ["eventSource" must be a boolean]]]'; + config.peers['peer0.org1.example.com'].eventSource = ''; + call.should.throw(err); + }); + }); + }); + + describe(prop('chaincodes'), () => { + it('should throw for missing required property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" is required]'; + delete config.chaincodes; + call.should.throw(err); + }); + + it('should throw for non-array value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" must be an array]'; + config.chaincodes = true; + call.should.throw(err); + }); + + it('should throw for an undefined item', () => { + const err = 'child "chaincodes" fails because ["chaincodes" must not be a sparse array]'; + config.chaincodes.push(undefined); + call.should.throw(err); + }); + + it('should throw "metadataPath" is set without "path"', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because ["metadataPath" missing required peer "path"]]'; + delete config.chaincodes[0].path; + call.should.throw(err); + }); + + it('should throw "path" is set without "language"', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 1 fails because ["path" missing required peer "language"]]'; + // on second chaincode + config.chaincodes[1].path = 'path'; + call.should.throw(err); + }); + + it('should throw "init" is set without "language"', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 1 fails because ["init" missing required peer "language"]]'; + // on second chaincode + config.chaincodes[1].init = []; + call.should.throw(err); + }); + + it('should throw "function" is set without "language"', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 1 fails because ["function" missing required peer "language"]]'; + // on second chaincode + config.chaincodes[1].function = 'init'; + call.should.throw(err); + }); + + it('should throw "initTransientMap" is set without "language"', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 1 fails because ["initTransientMap" missing required peer "language"]]'; + // on second chaincode + config.chaincodes[1].initTransientMap = {}; + call.should.throw(err); + }); + + it('should throw "collections-config" is set without "language"', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 1 fails because ["collections-config" missing required peer "language"]]'; + // on second chaincode + config.chaincodes[1]['collections-config'] = [{ + name: 'name', + policy: { + identities: [ + {role: {name: 'member', mspId: 'Org1MSP'}}, + {role: {name: 'member', mspId: 'Org2MSP'}} + ], + policy: { + '2-of': [ + {'signed-by': 1}, + {'1-of': [{'signed-by': 0}, {'signed-by': 1}]} + ] + } + }, + requiredPeerCount: 1, + maxPeerCount: 2, + blockToLive: 0 + }]; + call.should.throw(err); + }); + + it('should throw "endorsement-policy" is set without "language"', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 1 fails because ["endorsement-policy" missing required peer "language"]]'; + // on second chaincode + config.chaincodes[1]['endorsement-policy'] = { + identities: [ + { role: { name: 'member', mspId: 'Org1MSP' }}, + { role: { name: 'member', mspId: 'Org2MSP' }} + ], + policy: { + '2-of': [ + { 'signed-by': 1}, + { '1-of': [{ 'signed-by': 0 }, { 'signed-by': 1 }]} + ] + } + }; + call.should.throw(err); + }); + + describe(prop('[item].id'), () => { + it('should throw for missing required property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "id" fails because ["id" is required]]]'; + delete config.chaincodes[0].id; + call.should.throw(err); + }); + + it('should throw for non-string value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "id" fails because ["id" must be a string]]]'; + config.chaincodes[0].id = true; + call.should.throw(err); + }); + + it('should throw for empty string value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "id" fails because ["id" is not allowed to be empty, "id" length must be at least 1 characters long]]]'; + config.chaincodes[0].id = ''; + call.should.throw(err); + }); + }); + + describe(prop('[item].version'), () => { + it('should throw for missing required property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "version" fails because ["version" is required]]]'; + delete config.chaincodes[0].version; + call.should.throw(err); + }); + + it('should throw for non-string value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "version" fails because ["version" must be a string]]]'; + config.chaincodes[0].version = true; + call.should.throw(err); + }); + + it('should throw for empty string value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "version" fails because ["version" is not allowed to be empty, "version" length must be at least 1 characters long]]]'; + config.chaincodes[0].version = ''; + call.should.throw(err); + }); + }); + + describe(prop('[item].contractID'), () => { + it('should not throw for missing optional property', () => { + delete config.chaincodes[0].contractID; + call.should.not.throw(); + }); + + it('should throw for non-string value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "contractID" fails because ["contractID" must be a string]]]'; + config.chaincodes[0].contractID = true; + call.should.throw(err); + }); + + it('should throw for empty string value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "contractID" fails because ["contractID" is not allowed to be empty, "contractID" length must be at least 1 characters long]]]'; + config.chaincodes[0].contractID = ''; + call.should.throw(err); + }); + + it('should throw for duplicate items based on "contractID" vs "contractID"', () => { + const err = 'child "chaincodes" fails because ["chaincodes" position 1 contains a duplicate value]'; + config.chaincodes[1].contractID = 'ContractMarbles'; + call.should.throw(err); + }); + + it('should throw for duplicate items based on "contractID" vs "id"', () => { + const err = 'child "chaincodes" fails because ["chaincodes" position 1 contains a duplicate value]'; + config.chaincodes[1].id = 'ContractMarbles'; + call.should.throw(err); + }); + + it('should throw for duplicate items based on "id" vs "contractID"', () => { + const err = 'child "chaincodes" fails because ["chaincodes" position 1 contains a duplicate value]'; + delete config.chaincodes[0].contractID; + config.chaincodes[1].contractID = 'marbles'; + call.should.throw(err); + }); + + it('should throw for duplicate items based on "id" vs "id"', () => { + const err = 'child "chaincodes" fails because ["chaincodes" position 1 contains a duplicate value]'; + delete config.chaincodes[0].contractID; + config.chaincodes[1].id = 'marbles'; + call.should.throw(err); + }); + }); + + // using the second chaincode + describe(prop('[item].language'), () => { + it('should not throw for missing optional property', () => { + delete config.chaincodes[1].language; + call.should.not.throw(); + }); + + it('should throw for non-string value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 1 fails because [child "language" fails because ["language" must be a string]]]'; + config.chaincodes[1].language = true; + call.should.throw(err); + }); + + it('should throw for empty string value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 1 fails because [child "language" fails because ["language" is not allowed to be empty, "language" must be one of [golang, node, java]]]]'; + config.chaincodes[1].language = ''; + call.should.throw(err); + }); + + it('should throw for invalid string value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 1 fails because [child "language" fails because ["language" must be one of [golang, node, java]]]]'; + config.chaincodes[1].language = 'noooode'; + call.should.throw(err); + }); + }); + + describe(prop('[item].path'), () => { + it('should not throw for missing optional property', () => { + delete config.chaincodes[0].metadataPath; + delete config.chaincodes[0].path; + call.should.not.throw(); + }); + + it('should throw for non-string value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "path" fails because ["path" must be a string]]]'; + config.chaincodes[0].path = true; + call.should.throw(err); + }); + + it('should throw for empty string value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "path" fails because ["path" is not allowed to be empty, "path" length must be at least 1 characters long]]]'; + config.chaincodes[0].path = ''; + call.should.throw(err); + }); + }); + + describe(prop('[item].metadataPath'), () => { + it('should not throw for missing optional property', () => { + delete config.chaincodes[0].metadataPath; + call.should.not.throw(); + }); + + it('should throw for non-string value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "metadataPath" fails because ["metadataPath" must be a string]]]'; + config.chaincodes[0].metadataPath = true; + call.should.throw(err); + }); + + it('should throw for empty string value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "metadataPath" fails because ["metadataPath" is not allowed to be empty, "metadataPath" length must be at least 1 characters long]]]'; + config.chaincodes[0].metadataPath = ''; + call.should.throw(err); + }); + }); + + describe(prop('[item].init'), () => { + it('should not throw for missing optional property', () => { + delete config.chaincodes[0].init; + call.should.not.throw(); + }); + + it('should throw for non-array value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "init" fails because ["init" must be an array]]]'; + config.chaincodes[0].init = true; + call.should.throw(err); + }); + + it('should throw for undefined item', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "init" fails because ["init" must not be a sparse array]]]'; + config.chaincodes[0].init.push(undefined); + call.should.throw(err); + }); + + it('should throw for non-string item', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "init" fails because ["init" at position 0 fails because ["0" must be a string]]]]'; + config.chaincodes[0].init.push(true); + call.should.throw(err); + }); + }); + + describe(prop('[item].function'), () => { + it('should not throw for missing optional property', () => { + delete config.chaincodes[0].function; + call.should.not.throw(); + }); + + it('should throw for non-string value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "function" fails because ["function" must be a string]]]'; + config.chaincodes[0].function = true; + call.should.throw(err); + }); + }); + + describe(prop('[item].initTransientMap'), () => { + it('should not throw for missing optional property', () => { + delete config.chaincodes[0].initTransientMap; + call.should.not.throw(); + }); + + it('should throw for non-object value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "initTransientMap" fails because ["initTransientMap" must be an object]]]'; + config.chaincodes[0].initTransientMap = true; + call.should.throw(err); + }); + + it('should throw for non-string child property key', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "initTransientMap" fails because [child "false" fails because ["false" must be a string]]]]'; + config.chaincodes[0].initTransientMap.false = true; + call.should.throw(err); + }); + + it('should throw for non-string child property value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "initTransientMap" fails because [child "key" fails because ["key" must be a string]]]]'; + config.chaincodes[0].initTransientMap.key = true; + call.should.throw(err); + }); + }); + + describe(prop('[item].collections-config'), () => { + it('should not throw for missing optional property', () => { + delete config.chaincodes[0]['collections-config']; + call.should.not.throw(); + }); + + it('should not throw for string form instead of object form', () => { + config.chaincodes[0]['collections-config'] = 'path'; + call.should.not.throw(); + }); + + it('should throw for non-array value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" must be an array]]]'; + config.chaincodes[0]['collections-config'] = true; + call.should.throw(err); + }); + + it('should throw for empty array value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" must contain at least 1 items]]]'; + config.chaincodes[0]['collections-config'] = []; + call.should.throw(err); + }); + + it('should throw for undefined item', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" must not be a sparse array]]]'; + config.chaincodes[0]['collections-config'].push(undefined); + call.should.throw(err); + }); + + it('should throw for unknown child property of item', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because ["unknown" is not allowed]]]]'; + config.chaincodes[0]['collections-config'][0].unknown = ''; + call.should.throw(err); + }); + + describe(prop('[item].name'), () => { + it('should throw for missing required property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "name" fails because ["name" is required]]]]]'; + delete config.chaincodes[0]['collections-config'][0].name; + call.should.throw(err); + }); + + it('should throw for non-string value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "name" fails because ["name" must be a string]]]]]'; + config.chaincodes[0]['collections-config'][0].name = true; + call.should.throw(err); + }); + + it('should throw for empty string value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "name" fails because ["name" is not allowed to be empty, "name" length must be at least 1 characters long]]]]]'; + config.chaincodes[0]['collections-config'][0].name = ''; + call.should.throw(err); + }); + + it('should throw for duplicate value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" position 1 contains a duplicate value]]]'; + config.chaincodes[0]['collections-config'][0].name = 'name2'; + call.should.throw(err); + }); + }); + + describe(prop('[item].requiredPeerCount'), () => { + it('should throw for missing required property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "requiredPeerCount" fails because ["requiredPeerCount" is required], child "maxPeerCount" fails because ["maxPeerCount" references "requiredPeerCount" which is not a number]]]]]'; + delete config.chaincodes[0]['collections-config'][0].requiredPeerCount; + call.should.throw(err); + }); + + it('should throw for non-integer value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "requiredPeerCount" fails because ["requiredPeerCount" must be an integer]]]]]'; + config.chaincodes[0]['collections-config'][0].requiredPeerCount = 0.14; + call.should.throw(err); + }); + + it('should throw for negative value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "requiredPeerCount" fails because ["requiredPeerCount" must be larger than or equal to 0]]]]]'; + config.chaincodes[0]['collections-config'][0].requiredPeerCount = -1; + call.should.throw(err); + }); + + it('should throw for value greater than "maxPeerCount"', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "requiredPeerCount" fails because ["requiredPeerCount" must be less than or equal to 2], child "maxPeerCount" fails because ["maxPeerCount" must be larger than or equal to 3]]]]]'; + config.chaincodes[0]['collections-config'][0].requiredPeerCount = 3; + call.should.throw(err); + }); + }); + + describe(prop('[item].maxPeerCount'), () => { + it('should throw for missing required property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "requiredPeerCount" fails because ["requiredPeerCount" references "maxPeerCount" which is not a number], child "maxPeerCount" fails because ["maxPeerCount" is required]]]]]'; + delete config.chaincodes[0]['collections-config'][0].maxPeerCount; + call.should.throw(err); + }); + + it('should throw for non-integer value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "maxPeerCount" fails because ["maxPeerCount" must be an integer]]]]]'; + config.chaincodes[0]['collections-config'][0].maxPeerCount = 3.14; + call.should.throw(err); + }); + + it('should throw for negative value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "requiredPeerCount" fails because ["requiredPeerCount" must be less than or equal to -1], child "maxPeerCount" fails because ["maxPeerCount" must be larger than or equal to 1]]]]]'; + config.chaincodes[0]['collections-config'][0].maxPeerCount = -1; + call.should.throw(err); + }); + + it('should throw for value less than "requiredPeerCount"', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "requiredPeerCount" fails because ["requiredPeerCount" must be less than or equal to 0], child "maxPeerCount" fails because ["maxPeerCount" must be larger than or equal to 1]]]]]'; + config.chaincodes[0]['collections-config'][0].maxPeerCount = 0; + call.should.throw(err); + }); + }); + + describe(prop('[item].blockToLive'), () => { + it('should throw for missing required property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "blockToLive" fails because ["blockToLive" is required]]]]]'; + delete config.chaincodes[0]['collections-config'][0].blockToLive; + call.should.throw(err); + }); + + it('should throw for non-integer value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "blockToLive" fails because ["blockToLive" must be an integer]]]]]'; + config.chaincodes[0]['collections-config'][0].blockToLive = 3.14; + call.should.throw(err); + }); + + it('should throw for negative value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "blockToLive" fails because ["blockToLive" must be larger than or equal to 0]]]]]'; + config.chaincodes[0]['collections-config'][0].blockToLive = -1; + call.should.throw(err); + }); + }); + + describe(prop('[item].policy'), () => { + it('should throw for missing required property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because ["policy" is required]]]]]'; + delete config.chaincodes[0]['collections-config'][0].policy; + call.should.throw(err); + }); + + it('should throw for non-object value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because ["policy" must be an object]]]]]'; + config.chaincodes[0]['collections-config'][0].policy = true; + call.should.throw(err); + }); + + it('should throw for unknown child property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because ["unknown" is not allowed]]]]]'; + config.chaincodes[0]['collections-config'][0].policy.unknown = ''; + call.should.throw(err); + }); + + describe(prop('identities'), () => { + it('should throw for missing required property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "identities" fails because ["identities" is required]]]]]]'; + delete config.chaincodes[0]['collections-config'][0].policy.identities; + call.should.throw(err); + }); + + it('should throw for non-array value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "identities" fails because ["identities" must be an array]]]]]]'; + config.chaincodes[0]['collections-config'][0].policy.identities = true; + call.should.throw(err); + }); + + it('should throw for undefined item', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "identities" fails because ["identities" must not be a sparse array]]]]]]'; + config.chaincodes[0]['collections-config'][0].policy.identities.push(undefined); + call.should.throw(err); + }); + + it('should throw for duplicate items', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "identities" fails because ["identities" position 2 contains a duplicate value]]]]]]'; + config.chaincodes[0]['collections-config'][0].policy.identities.push({role: {name: 'member', mspId: 'Org1MSP'}}); + call.should.throw(err); + }); + + it('should throw for unknown child property of items', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "identities" fails because ["identities" at position 0 fails because ["unknown" is not allowed]]]]]]]'; + config.chaincodes[0]['collections-config'][0].policy.identities[0].unknown = ''; + call.should.throw(err); + }); + + describe(prop('[item].role'), () => { + it('should throw for missing required property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "identities" fails because ["identities" at position 0 fails because [child "role" fails because ["role" is required]]]]]]]]'; + delete config.chaincodes[0]['collections-config'][0].policy.identities[0].role; + call.should.throw(err); + }); + + it('should throw for unknown child property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "identities" fails because ["identities" at position 0 fails because [child "role" fails because ["unknown" is not allowed]]]]]]]]'; + config.chaincodes[0]['collections-config'][0].policy.identities[0].role.unknown = ''; + call.should.throw(err); + }); + + describe(prop('name'), () => { + it('should throw for missing required property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "identities" fails because ["identities" at position 0 fails because [child "role" fails because [child "name" fails because ["name" is required]]]]]]]]]'; + delete config.chaincodes[0]['collections-config'][0].policy.identities[0].role.name; + call.should.throw(err); + }); + + it('should throw for non-string value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "identities" fails because ["identities" at position 0 fails because [child "role" fails because [child "name" fails because ["name" must be a string]]]]]]]]]'; + config.chaincodes[0]['collections-config'][0].policy.identities[0].role.name = true; + call.should.throw(err); + }); + + it('should throw for invalid value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "identities" fails because ["identities" at position 0 fails because [child "role" fails because [child "name" fails because ["name" must be one of [member, admin]]]]]]]]]]'; + config.chaincodes[0]['collections-config'][0].policy.identities[0].role.name = 'not-member'; + call.should.throw(err); + }); + }); + + describe(prop('mspId'), () => { + it('should throw for missing required property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "identities" fails because ["identities" at position 0 fails because [child "role" fails because [child "mspId" fails because ["mspId" is required]]]]]]]]]'; + delete config.chaincodes[0]['collections-config'][0].policy.identities[0].role.mspId; + call.should.throw(err); + }); + + it('should throw for non-string value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "identities" fails because ["identities" at position 0 fails because [child "role" fails because [child "mspId" fails because ["mspId" must be a string]]]]]]]]]'; + config.chaincodes[0]['collections-config'][0].policy.identities[0].role.mspId = true; + call.should.throw(err); + }); + + it('should throw for invalid value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "identities" fails because ["identities" at position 0 fails because [child "role" fails because [child "mspId" fails because ["mspId" must be one of [Org1MSP, Org2MSP]]]]]]]]]]'; + config.chaincodes[0]['collections-config'][0].policy.identities[0].role.mspId = 'Org5MSP'; + call.should.throw(err); + }); + }); + }); + }); + + describe(prop('policy'), () => { + it('should throw for missing required property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "policy" fails because ["policy" is required]]]]]]'; + delete config.chaincodes[0]['collections-config'][0].policy.policy; + call.should.throw(err); + }); + + it('should throw for non-object value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "policy" fails because ["policy" must be an object]]]]]]'; + config.chaincodes[0]['collections-config'][0].policy.policy = true; + call.should.throw(err); + }); + + it('should throw for an empty value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "policy" fails because ["policy" must have 1 children]]]]]]'; + config.chaincodes[0]['collections-config'][0].policy.policy = {}; + call.should.throw(err); + }); + + it('should throw for an invalid child property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "policy" fails because ["of2" is not allowed]]]]]]'; + config.chaincodes[0]['collections-config'][0].policy.policy.of2 = {}; + call.should.throw(err); + }); + + describe(prop('X-of'), () => { + it('should throw for non-array value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "policy" fails because [child "2-of" fails because ["2-of" must be an array]]]]]]]'; + config.chaincodes[0]['collections-config'][0].policy.policy['2-of'] = true; + call.should.throw(err); + }); + + it('should throw for empty value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "policy" fails because [child "2-of" fails because ["2-of" must contain at least 1 items]]]]]]]'; + config.chaincodes[0]['collections-config'][0].policy.policy['2-of'] = []; + call.should.throw(err); + }); + + it('should throw for undefined item', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "policy" fails because [child "2-of" fails because ["2-of" must not be a sparse array]]]]]]]'; + config.chaincodes[0]['collections-config'][0].policy.policy['2-of'].push(undefined); + call.should.throw(err); + }); + + it('should throw for item with invalid key', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "policy" fails because [child "2-of" fails because ["2-of" at position 2 fails because ["of2" is not allowed]]]]]]]]'; + config.chaincodes[0]['collections-config'][0].policy.policy['2-of'].push({ of2: true }); + call.should.throw(err); + }); + + it('should throw for empty item', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "policy" fails because [child "2-of" fails because ["2-of" at position 2 fails because ["2" must have at least 1 children]]]]]]]]'; + config.chaincodes[0]['collections-config'][0].policy.policy['2-of'].push({}); + call.should.throw(err); + }); + + // the recursive X-of items are covered by the above tests + describe(prop('[item].signed-by'), () => { + it('should throw for non-integer value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "policy" fails because [child "2-of" fails because ["2-of" at position 0 fails because [child "signed-by" fails because ["signed-by" must be an integer]]]]]]]]]'; + config.chaincodes[0]['collections-config'][0].policy.policy['2-of'][0]['signed-by'] = 3.14; + call.should.throw(err); + }); + + it('should throw for negative value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "collections-config" fails because ["collections-config" must be a string, "collections-config" at position 0 fails because [child "policy" fails because [child "policy" fails because [child "2-of" fails because ["2-of" at position 0 fails because [child "signed-by" fails because ["signed-by" must be larger than or equal to 0]]]]]]]]]'; + config.chaincodes[0]['collections-config'][0].policy.policy['2-of'][0]['signed-by'] = -10; + call.should.throw(err); + }); + }); + }); + + }); + }); + }); + + describe(prop('[item].endorsement-policy'), () => { + it('should not throw for missing optional property', () => { + delete config.chaincodes[0]['endorsement-policy']; + call.should.not.throw(); + }); + + it('should throw for non-object value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because ["endorsement-policy" must be an object]]]'; + config.chaincodes[0]['endorsement-policy'] = true; + call.should.throw(err); + }); + + it('should throw for unknown child property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because ["unknown" is not allowed]]]'; + config.chaincodes[0]['endorsement-policy'].unknown = ''; + call.should.throw(err); + }); + + describe(prop('identities'), () => { + it('should throw for missing required property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "identities" fails because ["identities" is required]]]]'; + delete config.chaincodes[0]['endorsement-policy'].identities; + call.should.throw(err); + }); + + it('should throw for non-array value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "identities" fails because ["identities" must be an array]]]]'; + config.chaincodes[0]['endorsement-policy'].identities = true; + call.should.throw(err); + }); + + it('should throw for undefined item', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "identities" fails because ["identities" must not be a sparse array]]]]'; + config.chaincodes[0]['endorsement-policy'].identities.push(undefined); + call.should.throw(err); + }); + + it('should throw for duplicate items', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "identities" fails because ["identities" position 2 contains a duplicate value]]]]'; + config.chaincodes[0]['endorsement-policy'].identities.push({ + role: { + name: 'member', + mspId: 'Org1MSP' + } + }); + call.should.throw(err); + }); + + it('should throw for unknown child property of items', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "identities" fails because ["identities" at position 0 fails because ["unknown" is not allowed]]]]]'; + config.chaincodes[0]['endorsement-policy'].identities[0].unknown = ''; + call.should.throw(err); + }); + + describe(prop('[item].role'), () => { + it('should throw for missing required property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "identities" fails because ["identities" at position 0 fails because [child "role" fails because ["role" is required]]]]]]'; + delete config.chaincodes[0]['endorsement-policy'].identities[0].role; + call.should.throw(err); + }); + + it('should throw for unknown child property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "identities" fails because ["identities" at position 0 fails because [child "role" fails because ["unknown" is not allowed]]]]]]'; + config.chaincodes[0]['endorsement-policy'].identities[0].role.unknown = ''; + call.should.throw(err); + }); + + describe(prop('name'), () => { + it('should throw for missing required property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "identities" fails because ["identities" at position 0 fails because [child "role" fails because [child "name" fails because ["name" is required]]]]]]]'; + delete config.chaincodes[0]['endorsement-policy'].identities[0].role.name; + call.should.throw(err); + }); + + it('should throw for non-string value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "identities" fails because ["identities" at position 0 fails because [child "role" fails because [child "name" fails because ["name" must be a string]]]]]]]'; + config.chaincodes[0]['endorsement-policy'].identities[0].role.name = true; + call.should.throw(err); + }); + + it('should throw for invalid value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "identities" fails because ["identities" at position 0 fails because [child "role" fails because [child "name" fails because ["name" must be one of [member, admin]]]]]]]]'; + config.chaincodes[0]['endorsement-policy'].identities[0].role.name = 'not-member'; + call.should.throw(err); + }); + }); + + describe(prop('mspId'), () => { + it('should throw for missing required property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "identities" fails because ["identities" at position 0 fails because [child "role" fails because [child "mspId" fails because ["mspId" is required]]]]]]]'; + delete config.chaincodes[0]['endorsement-policy'].identities[0].role.mspId; + call.should.throw(err); + }); + + it('should throw for non-string value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "identities" fails because ["identities" at position 0 fails because [child "role" fails because [child "mspId" fails because ["mspId" must be a string]]]]]]]'; + config.chaincodes[0]['endorsement-policy'].identities[0].role.mspId = true; + call.should.throw(err); + }); + + it('should throw for invalid value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "identities" fails because ["identities" at position 0 fails because [child "role" fails because [child "mspId" fails because ["mspId" must be one of [Org1MSP, Org2MSP]]]]]]]]'; + config.chaincodes[0]['endorsement-policy'].identities[0].role.mspId = 'Org5MSP'; + call.should.throw(err); + }); + }); + }); + }); + + describe(prop('policy'), () => { + it('should throw for missing required property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "policy" fails because ["policy" is required]]]]'; + delete config.chaincodes[0]['endorsement-policy'].policy; + call.should.throw(err); + }); + + it('should throw for non-object value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "policy" fails because ["policy" must be an object]]]]'; + config.chaincodes[0]['endorsement-policy'].policy = true; + call.should.throw(err); + }); + + it('should throw for an empty value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "policy" fails because ["policy" must have 1 children]]]]'; + config.chaincodes[0]['endorsement-policy'].policy = {}; + call.should.throw(err); + }); + + it('should throw for an invalid child property', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "policy" fails because ["of2" is not allowed]]]]'; + config.chaincodes[0]['endorsement-policy'].policy.of2 = {}; + call.should.throw(err); + }); + + describe(prop('X-of'), () => { + it('should throw for non-array value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "policy" fails because [child "2-of" fails because ["2-of" must be an array]]]]]'; + config.chaincodes[0]['endorsement-policy'].policy['2-of'] = true; + call.should.throw(err); + }); + + it('should throw for empty value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "policy" fails because [child "2-of" fails because ["2-of" must contain at least 1 items]]]]]'; + config.chaincodes[0]['endorsement-policy'].policy['2-of'] = []; + call.should.throw(err); + }); + + it('should throw for undefined item', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "policy" fails because [child "2-of" fails because ["2-of" must not be a sparse array]]]]]'; + config.chaincodes[0]['endorsement-policy'].policy['2-of'].push(undefined); + call.should.throw(err); + }); + + it('should throw for item with invalid key', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "policy" fails because [child "2-of" fails because ["2-of" at position 2 fails because ["of2" is not allowed]]]]]]'; + config.chaincodes[0]['endorsement-policy'].policy['2-of'].push({of2: true}); + call.should.throw(err); + }); + + it('should throw for empty item', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "policy" fails because [child "2-of" fails because ["2-of" at position 2 fails because ["2" must have at least 1 children]]]]]]'; + config.chaincodes[0]['endorsement-policy'].policy['2-of'].push({}); + call.should.throw(err); + }); + + // the recursive X-of items are covered by the above tests + describe(prop('[item].signed-by'), () => { + it('should throw for non-integer value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "policy" fails because [child "2-of" fails because ["2-of" at position 0 fails because [child "signed-by" fails because ["signed-by" must be an integer]]]]]]]'; + config.chaincodes[0]['endorsement-policy'].policy['2-of'][0]['signed-by'] = 3.14; + call.should.throw(err); + }); + + it('should throw for negative value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "endorsement-policy" fails because [child "policy" fails because [child "2-of" fails because ["2-of" at position 0 fails because [child "signed-by" fails because ["signed-by" must be larger than or equal to 0]]]]]]]'; + config.chaincodes[0]['endorsement-policy'].policy['2-of'][0]['signed-by'] = -10; + call.should.throw(err); + }); + }); + }); + }); + }); + + describe(prop('[item].targetPeers'), () => { + it('should not throw for missing optional property', () => { + delete config.chaincodes[0].targetPeers; + call.should.not.throw(); + }); + + it('should throw for non-array value', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "targetPeers" fails because ["targetPeers" must be an array]]]'; + config.chaincodes[0].targetPeers = true; + call.should.throw(err); + }); + + it('should throw for empty array', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "targetPeers" fails because ["targetPeers" must contain at least 1 items]]]'; + config.chaincodes[0].targetPeers = []; + call.should.throw(err); + }); + + it('should throw for undefined item', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "targetPeers" fails because ["targetPeers" must not be a sparse array]]]'; + config.chaincodes[0].targetPeers.push(undefined); + call.should.throw(err); + }); + + it('should throw for invalid peer reference', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "targetPeers" fails because ["targetPeers" at position 2 fails because ["2" must be one of [peer0.org1.example.com, peer0.org2.example.com]]]]]'; + config.chaincodes[0].targetPeers.push('peer0.org5.example.com'); + call.should.throw(err); + }); + + it('should throw for duplicate peer reference', () => { + const err = 'child "chaincodes" fails because ["chaincodes" at position 0 fails because [child "targetPeers" fails because ["targetPeers" position 2 contains a duplicate value]]]'; + config.chaincodes[0].targetPeers.push('peer0.org1.example.com'); + call.should.throw(err); + }); + }); + }); + }); +}); diff --git a/packages/caliper-samples/network/fabric-v1.4/2org1peercouchdb/fabric-node.yaml b/packages/caliper-samples/network/fabric-v1.4/2org1peercouchdb/fabric-node.yaml index 1b4ccae2b..639791953 100644 --- a/packages/caliper-samples/network/fabric-v1.4/2org1peercouchdb/fabric-node.yaml +++ b/packages/caliper-samples/network/fabric-v1.4/2org1peercouchdb/fabric-node.yaml @@ -10,7 +10,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -# +# name: Fabric version: "1.0" @@ -61,7 +61,7 @@ channels: definition: capabilities: [] consortium: 'SampleConsortium' - msps: ['Org1MSP', 'Org2MSP', 'Org3MSP'] + msps: ['Org1MSP', 'Org2MSP'] version: 0 orderers: - orderer.example.com