From ea17a6ace16bcc59f5b0d5914b5d6844f95d3b1f Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Tue, 9 Dec 2014 18:29:29 -0300 Subject: [PATCH] Add a preconditions module, and refactor errors 100% code coverage for the preconditions module. Usage: ``` $.checkState(something === anotherthing, 'Expected something to be anotherthing'); $.checkArgument(something < 100, 'something', 'must be less than 100'); $.checkArgumentType(something, PrivateKey, 'something'); // The third argument is a helper to mention the name of the argument $.checkArgumentType(something, PrivateKey); // but it's optional (will show up as "(unknown argument)") ``` --- lib/errors/build.js | 12 +-- lib/errors/spec.js | 157 ++++++++++++++++++------------------- lib/hdprivatekey.js | 20 ++--- lib/hdpublickey.js | 19 ++--- lib/util/preconditions.js | 29 +++++++ test/hdprivatekey.js | 19 ++--- test/hdpublickey.js | 27 ++++--- test/util/preconditions.js | 61 ++++++++++++++ 8 files changed, 212 insertions(+), 132 deletions(-) create mode 100644 lib/util/preconditions.js create mode 100644 test/util/preconditions.js diff --git a/lib/errors/build.js b/lib/errors/build.js index cbd44f9fa..332f68aae 100644 --- a/lib/errors/build.js +++ b/lib/errors/build.js @@ -3,17 +3,9 @@ var _ = require('lodash'); var fs = require('fs'); -var formatMessage = function(message) { - message = '\'' + message + '\''; - for (var i = 0; i < 3; i++) { - message += '.replace(\'{' + i + '}\', arguments[' + i + '])'; - } - return message; -}; - var defineElement = function(fullName, baseClass, message) { return fullName + ' = function() {\n' + - ' this.message = ' + formatMessage(message) + ';\n' + + ' this.message = ' + message + ';\n' + ' this.stack = this.message + \'\\n\' + (new Error()).stack;\n' + '};\n' + fullName + '.prototype = Object.create(' + baseClass + '.prototype);\n' + @@ -45,7 +37,7 @@ var traverseRoot = function(errorsDefinition) { var generated = '\'use strict\';\n\nvar inherits = require(\'inherits\');\n\n'; generated += '/** AUTOGENERATED FILE. DON\'T EDIT, MODIFY "lib/errors/spec.js" INSTEAD */\n\n'; generated += 'var bitcore = {};\n\n'; - generated += defineElement(fullName, path, 'Internal error'); + generated += defineElement(fullName, path, '\'Internal error\''); generated += childDefinitions(fullName, errorsDefinition); generated += 'module.exports = bitcore.Error;\n'; return generated; diff --git a/lib/errors/spec.js b/lib/errors/spec.js index 6a0453b57..b1d2a7c19 100644 --- a/lib/errors/spec.js +++ b/lib/errors/spec.js @@ -1,85 +1,80 @@ +'use strict'; + +function format(arg) { + return '\'' + arg + .replace('{0}', '\' + arguments[0] + \'') + .replace('{1}', '\' + arguments[1] + \'') + .replace('{2}', '\' + arguments[2] + \'') + '\''; +} + module.exports = [{ - name: 'HDPrivateKey', - message: 'Internal Error on HDPrivateKey {0}', - errors: [ - { - name: 'InvalidArgument', - message: 'Invalid Argument {0}, expected {1} but got {2}', + name: 'InvalidB58Char', + message: format('Invalid Base58 character: {0} in {1}') + }, { + name: 'InvalidB58Checksum', + message: format('Invalid Base58 checksum for {0}') + }, { + name: 'InvalidNetwork', + message: format('Invalid version for network: got {0}') + }, { + name: 'InvalidState', + message: format('Invalid state: {0}') + }, { + name: 'InvalidNetworkArgument', + message: format('Invalid network: must be "livenet" or "testnet", got {0}') + }, { + name: 'InvalidArgument', + message: format('Invalid Argument {0}, {1}'), + }, { + name: 'InvalidArgumentType', + message: format('Invalid Argument for {2}, expected {1} but got ') + '+ typeof arguments[0]', + }, { + name: 'HDPrivateKey', + message: format('Internal Error on HDPrivateKey {0}'), + errors: [{ + name: 'InvalidDerivationArgument', + message: format('Invalid derivation argument {0}, expected string, or number and boolean') + }, { + name: 'InvalidEntropyArgument', + message: format('Invalid entropy: must be an hexa string or binary buffer, got {0}'), errors: [{ - name: 'InvalidB58Char', - message: 'Invalid Base58 character: {0} in {1}' + name: 'TooMuchEntropy', + message: format('Invalid entropy: more than 512 bits is non standard, got "{0}"') }, { - name: 'InvalidB58Checksum', - message: 'Invalid Base58 checksum for {0}' - }, { - name: 'InvalidDerivationArgument', - message: 'Invalid derivation argument {0}, expected string, or number and boolean' - }, { - name: 'InvalidEntropyArgument', - message: 'Invalid entropy: must be an hexa string or binary buffer, got {0}', - errors: [{ - name: 'TooMuchEntropy', - message: 'Invalid entropy: more than 512 bits is non standard, got "{0}"' - }, { - name: 'NotEnoughEntropy', - message: 'Invalid entropy: at least 128 bits needed, got "{0}"' - }] - }, { - name: 'InvalidLength', - message: 'Invalid length for xprivkey string in {0}' - }, { - name: 'InvalidNetwork', - message: 'Invalid version for network: got {0}' - }, { - name: 'InvalidNetworkArgument', - message: 'Invalid network: must be "livenet" or "testnet", got {0}' - }, { - name: 'InvalidPath', - message: 'Invalid derivation path: {0}' - }, { - name: 'UnrecognizedArgument', - message: 'Invalid argument: creating a HDPrivateKey requires a string, buffer, json or object, got "{0}"' + name: 'NotEnoughEntropy', + message: format('Invalid entropy: at least 128 bits needed, got "{0}"') }] - } - ] -}, { - name: 'HDPublicKey', - message: 'Internal Error on HDPublicKey {0}', - errors: [ - { - name: 'InvalidArgument', - message: 'Invalid Argument {0}, expected {1} but got {2}', - errors: [{ - name: 'ArgumentIsPrivateExtended', - message: 'Argument is an extended private key: {0}' - }, { - name: 'InvalidB58Char', - message: 'Invalid Base58 character: {0} in {1}' - }, { - name: 'InvalidB58Checksum', - message: 'Invalid Base58 checksum for {0}' - }, { - name: 'InvalidDerivationArgument', - message: 'Invalid derivation argument: got {0}' - }, { - name: 'InvalidLength', - message: 'Invalid length for xpubkey: got "{0}"' - }, { - name: 'InvalidNetwork', - message: 'Invalid network, expected a different version: got "{0}"' - }, { - name: 'InvalidNetworkArgument', - message: 'Expected network to be "livenet" or "testnet", got "{0}"' - }, { - name: 'InvalidPath', - message: 'Invalid derivation path, it should look like: "m/1/100", got "{0}"' - }, { - name: 'MustSupplyArgument', - message: 'Must supply an argument to create a HDPublicKey' - }, { - name: 'UnrecognizedArgument', - message: 'Invalid argument for creation, must be string, json, buffer, or object' - }] - } - ] -}]; + }, { + name: 'InvalidLength', + message: format('Invalid length for xprivkey string in {0}') + }, { + name: 'InvalidPath', + message: format('Invalid derivation path: {0}') + }, { + name: 'UnrecognizedArgument', + message: format('Invalid argument: creating a HDPrivateKey requires a string, buffer, json or object, got "{0}"') + }] + }, { + name: 'HDPublicKey', + message: format('Internal Error on HDPublicKey {0}'), + errors: [{ + name: 'ArgumentIsPrivateExtended', + message: format('Argument is an extended private key: {0}') + }, { + name: 'InvalidDerivationArgument', + message: format('Invalid derivation argument: got {0}') + }, { + name: 'InvalidLength', + message: format('Invalid length for xpubkey: got "{0}"') + }, { + name: 'InvalidPath', + message: format('Invalid derivation path, it should look like: "m/1/100", got "{0}"') + }, { + name: 'MustSupplyArgument', + message: format('Must supply an argument to create a HDPublicKey') + }, { + name: 'UnrecognizedArgument', + message: format('Invalid argument for creation, must be string, json, buffer, or object') + }] + } +]; diff --git a/lib/hdprivatekey.js b/lib/hdprivatekey.js index 2e357281e..3b478b815 100644 --- a/lib/hdprivatekey.js +++ b/lib/hdprivatekey.js @@ -15,8 +15,8 @@ var Point = require('./crypto/point'); var PrivateKey = require('./privatekey'); var Random = require('./crypto/random'); -var bitcoreErrors = require('./errors'); -var errors = bitcoreErrors.HDPrivateKey.InvalidArgument; +var errors = require('./errors'); +var hdErrors = errors.HDPrivateKey; var bufferUtil = require('./util/buffer'); var jsUtil = require('./util/js'); @@ -54,7 +54,7 @@ function HDPrivateKey(arg) { if (_.isObject(arg)) { this._buildFromObject(arg); } else { - throw new errors.UnrecognizedArgument(arg); + throw new hdErrors.UnrecognizedArgument(arg); } } } else { @@ -89,7 +89,7 @@ HDPrivateKey.prototype.derive = function(arg, hardened) { } else if (_.isString(arg)) { return this._deriveFromString(arg); } else { - throw new errors.InvalidDerivationArgument(arg); + throw new hdErrors.InvalidDerivationArgument(arg); } }; @@ -140,7 +140,7 @@ HDPrivateKey.prototype._deriveFromString = function(path) { return this; } if (!_.contains(HDPrivateKey.RootElementAlias, steps[0])) { - throw new errors.InvalidPath(path); + throw new hdErrors.InvalidPath(path); } steps = steps.slice(1); @@ -178,7 +178,7 @@ HDPrivateKey.isValidSerialized = function(data, network) { HDPrivateKey.getSerializedError = function(data, network) { /* jshint maxcomplexity: 10 */ if (!(_.isString(data) || bufferUtil.isBuffer(data))) { - return new errors.UnrecognizedArgument('Expected string or buffer'); + return new hdErrors.UnrecognizedArgument('Expected string or buffer'); } if (!Base58.validCharacters(data)) { return new errors.InvalidB58Char('(unknown)', data); @@ -189,7 +189,7 @@ HDPrivateKey.getSerializedError = function(data, network) { return new errors.InvalidB58Checksum(data); } if (data.length !== HDPrivateKey.DataLength) { - return new errors.InvalidLength(data); + return new hdErrors.InvalidLength(data); } if (!_.isUndefined(network)) { var error = HDPrivateKey._validateNetwork(data, network); @@ -265,13 +265,13 @@ HDPrivateKey.fromSeed = function(hexa, network) { hexa = bufferUtil.hexToBuffer(hexa); } if (!Buffer.isBuffer(hexa)) { - throw new errors.InvalidEntropyArgument(hexa); + throw new hdErrors.InvalidEntropyArgument(hexa); } if (hexa.length < MINIMUM_ENTROPY_BITS * BITS_TO_BYTES) { - throw new errors.InvalidEntropyArgument.NotEnoughEntropy(hexa); + throw new hdErrors.InvalidEntropyArgument.NotEnoughEntropy(hexa); } if (hexa.length > MAXIMUM_ENTROPY_BITS * BITS_TO_BYTES) { - throw new errors.InvalidEntropyArgument.TooMuchEntropy(hexa); + throw new hdErrors.InvalidEntropyArgument.TooMuchEntropy(hexa); } var hash = Hash.sha512hmac(hexa, new buffer.Buffer('Bitcoin seed')); diff --git a/lib/hdpublickey.js b/lib/hdpublickey.js index 1b361d5ea..a11e0c0eb 100644 --- a/lib/hdpublickey.js +++ b/lib/hdpublickey.js @@ -12,7 +12,8 @@ var Point = require('./crypto/point'); var PublicKey = require('./publickey'); var bitcoreErrors = require('./errors'); -var errors = bitcoreErrors.HDPublicKey.InvalidArgument; +var errors = bitcoreErrors; +var hdErrors = bitcoreErrors.HDPublicKey; var assert = require('assert'); var jsUtil = require('./util/js'); @@ -43,7 +44,7 @@ function HDPublicKey(arg) { } else if (jsUtil.isValidJson(arg)) { return this._buildFromJson(arg); } else { - if (error instanceof errors.ArgumentIsPrivateExtended) { + if (error instanceof hdErrors.ArgumentIsPrivateExtended) { return new HDPrivateKey(arg).hdPublicKey; } throw error; @@ -56,11 +57,11 @@ function HDPublicKey(arg) { return this._buildFromObject(arg); } } else { - throw new errors.UnrecognizedArgument(arg); + throw new hdErrors.UnrecognizedArgument(arg); } } } else { - throw new errors.MustSupplyArgument(); + throw new hdErrors.MustSupplyArgument(); } } @@ -91,13 +92,13 @@ HDPublicKey.prototype.derive = function (arg, hardened) { } else if (_.isString(arg)) { return this._deriveFromString(arg); } else { - throw new errors.InvalidDerivationArgument(arg); + throw new hdErrors.InvalidDerivationArgument(arg); } }; HDPublicKey.prototype._deriveWithNumber = function (index, hardened) { if (hardened || index >= HDPublicKey.Hardened) { - throw new errors.InvalidIndexCantDeriveHardened(); + throw new hdErrors.InvalidIndexCantDeriveHardened(); } var cached = HDKeyCache.get(this.xpubkey, index, hardened); if (cached) { @@ -133,7 +134,7 @@ HDPublicKey.prototype._deriveFromString = function (path) { return this; } if (!_.contains(HDPublicKey.RootElementAlias, steps[0])) { - throw new errors.InvalidPath(path); + throw new hdErrors.InvalidPath(path); } steps = steps.slice(1); @@ -172,7 +173,7 @@ HDPublicKey.getSerializedError = function (data, network) { /* jshint maxcomplexity: 10 */ /* jshint maxstatements: 20 */ if (!(_.isString(data) || bufferUtil.isBuffer(data))) { - return new errors.UnrecognizedArgument('expected buffer or string'); + return new hdErrors.UnrecognizedArgument('expected buffer or string'); } if (!Base58.validCharacters(data)) { return new errors.InvalidB58Char('(unknown)', data); @@ -193,7 +194,7 @@ HDPublicKey.getSerializedError = function (data, network) { } network = Network.get(network) || Network.defaultNetwork; if (bufferUtil.integerFromBuffer(data.slice(0, 4)) === network.xprivkey) { - return new errors.ArgumentIsPrivateExtended(); + return new hdErrors.ArgumentIsPrivateExtended(); } return null; }; diff --git a/lib/util/preconditions.js b/lib/util/preconditions.js new file mode 100644 index 000000000..434a30630 --- /dev/null +++ b/lib/util/preconditions.js @@ -0,0 +1,29 @@ +'use strict'; + +var errors = require('../errors'); +var _ = require('lodash'); + +module.exports = { + checkState: function(condition, message) { + if (!condition) { + throw new errors.InvalidState(message); + } + }, + checkArgument: function(condition, argumentName, message) { + if (!condition) { + throw new errors.InvalidArgument(argumentName, message); + } + }, + checkArgumentType: function(argument, type, argumentName) { + argumentName = argumentName || '(unknown name)'; + if (_.isString(type)) { + if (typeof argument !== type) { + throw new errors.InvalidArgumentType(argument, type, argumentName); + } + } else { + if (!(argument instanceof type)) { + throw new errors.InvalidArgumentType(argument, type.name, argumentName); + } + } + } +}; diff --git a/test/hdprivatekey.js b/test/hdprivatekey.js index 6d8a7eaa7..80037cbfc 100644 --- a/test/hdprivatekey.js +++ b/test/hdprivatekey.js @@ -5,7 +5,8 @@ var assert = require('assert'); var should = require('chai').should(); var expect = require('chai').expect; var bitcore = require('..'); -var errors = bitcore.errors.HDPrivateKey.InvalidArgument; +var errors = bitcore.errors; +var hdErrors = errors.HDPrivateKey; var buffer = require('buffer'); var bufferUtil = bitcore.util.buffer; var HDPrivateKey = bitcore.HDPrivateKey; @@ -67,7 +68,7 @@ describe('HDPrivate key interface', function() { describe('should error with a nonsensical argument', function() { it('like a number', function() { - expectFailBuilding(1, errors.UnrecognizedArgument); + expectFailBuilding(1, hdErrors.UnrecognizedArgument); }); }); @@ -81,11 +82,11 @@ describe('HDPrivate key interface', function() { }); it('fails when trying to derive with an invalid argument', function() { - expectDerivationFail([], errors.InvalidDerivationArgument); + expectDerivationFail([], hdErrors.InvalidDerivationArgument); }); it('catches early invalid paths', function() { - expectDerivationFail('s', errors.InvalidPath); + expectDerivationFail('s', hdErrors.InvalidPath); }); it('allows derivation of hardened keys by passing a very big number', function() { @@ -103,14 +104,14 @@ describe('HDPrivate key interface', function() { it('returns InvalidArgument if invalid data is given to getSerializedError', function() { expect( HDPrivateKey.getSerializedError(1) instanceof - errors.UnrecognizedArgument + hdErrors.UnrecognizedArgument ).to.equal(true); }); it('returns InvalidLength if data of invalid length is given to getSerializedError', function() { expect( HDPrivateKey.getSerializedError(Base58Check.encode(new buffer.Buffer('onestring'))) instanceof - errors.InvalidLength + hdErrors.InvalidLength ).to.equal(true); }); @@ -137,17 +138,17 @@ describe('HDPrivate key interface', function() { HDPrivateKey.fromSeed('01234567890abcdef01234567890abcdef').xprivkey.should.exist(); }); it('fails when argument is not a buffer or string', function() { - expectSeedFail(1, errors.InvalidEntropyArgument); + expectSeedFail(1, hdErrors.InvalidEntropyArgument); }); it('fails when argument doesn\'t provide enough entropy', function() { - expectSeedFail('01', errors.InvalidEntropyArgument.NotEnoughEntropy); + expectSeedFail('01', hdErrors.InvalidEntropyArgument.NotEnoughEntropy); }); it('fails when argument provides too much entropy', function() { var entropy = '0'; for (var i = 0; i < 129; i++) { entropy += '1'; } - expectSeedFail(entropy, errors.InvalidEntropyArgument.TooMuchEntropy); + expectSeedFail(entropy, hdErrors.InvalidEntropyArgument.TooMuchEntropy); }); }); diff --git a/test/hdpublickey.js b/test/hdpublickey.js index 5dc8a747d..9ab06ddad 100644 --- a/test/hdpublickey.js +++ b/test/hdpublickey.js @@ -7,7 +7,8 @@ var should = require('chai').should(); var expect = require('chai').expect; var bitcore = require('..'); var buffer = require('buffer'); -var errors = bitcore.errors.HDPublicKey.InvalidArgument; +var errors = bitcore.errors; +var hdErrors = bitcore.errors.HDPublicKey; var bufferUtil = bitcore.util.buffer; var HDPrivateKey = bitcore.HDPrivateKey; var HDPublicKey = bitcore.HDPublicKey; @@ -65,12 +66,12 @@ describe('HDPublicKey interface', function() { }); it('fails when user doesn\'t supply an argument', function() { - expectFailBuilding(null, errors.MustSupplyArgument); + expectFailBuilding(null, hdErrors.MustSupplyArgument); }); it('doesn\'t recognize an invalid argument', function() { - expectFailBuilding(1, errors.UnrecognizedArgument); - expectFailBuilding(true, errors.UnrecognizedArgument); + expectFailBuilding(1, hdErrors.UnrecognizedArgument); + expectFailBuilding(true, hdErrors.UnrecognizedArgument); }); @@ -78,7 +79,7 @@ describe('HDPublicKey interface', function() { it('fails on invalid length', function() { expectFailBuilding( Base58Check.encode(new buffer.Buffer([1, 2, 3])), - errors.InvalidLength + hdErrors.InvalidLength ); }); it('fails on invalid base58 encoding', function() { @@ -122,7 +123,7 @@ describe('HDPublicKey interface', function() { expect(a instanceof b).to.equal(true); }; it('throws invalid argument when argument is not a string or buffer', function() { - compareType(HDPublicKey.getSerializedError(1), errors.UnrecognizedArgument); + compareType(HDPublicKey.getSerializedError(1), hdErrors.UnrecognizedArgument); }); it('if a network is provided, validates that data corresponds to it', function() { compareType(HDPublicKey.getSerializedError(xpubkey, 'testnet'), errors.InvalidNetwork); @@ -160,27 +161,27 @@ describe('HDPublicKey interface', function() { it('doesn\'t allow object arguments for derivation', function() { expectFail(function() { return new HDPublicKey(xpubkey).derive({}); - }, errors.InvalidDerivationArgument); + }, hdErrors.InvalidDerivationArgument); }); it('needs first argument for derivation', function() { expectFail(function() { return new HDPublicKey(xpubkey).derive('s'); - }, errors.InvalidPath); + }, hdErrors.InvalidPath); }); it('doesn\'t allow other parameters like m\' or M\' or "s"', function() { /* jshint quotmark: double */ - expectDerivationFail("m'", errors.InvalidDerivationArgument); - expectDerivationFail("M'", errors.InvalidDerivationArgument); - expectDerivationFail("1", errors.InvalidDerivationArgument); - expectDerivationFail("S", errors.InvalidDerivationArgument); + expectDerivationFail("m'", hdErrors.InvalidDerivationArgument); + expectDerivationFail("M'", hdErrors.InvalidDerivationArgument); + expectDerivationFail("1", hdErrors.InvalidDerivationArgument); + expectDerivationFail("S", hdErrors.InvalidDerivationArgument); }); it('can\'t derive hardened keys', function() { expectFail(function() { return new HDPublicKey(xpubkey).derive(HDPublicKey.Hardened + 1); - }, errors.InvalidDerivationArgument); + }, hdErrors.InvalidDerivationArgument); }); it('should use the cache', function() { diff --git a/test/util/preconditions.js b/test/util/preconditions.js new file mode 100644 index 000000000..653b1843c --- /dev/null +++ b/test/util/preconditions.js @@ -0,0 +1,61 @@ +'use strict'; + +var errors = require('../../lib/errors'); +var $ = require('../../lib/util/preconditions'); +var PrivateKey = require('../../lib/privatekey'); + +describe('preconditions', function() { + + it('can be used to assert state', function() { + (function() { + $.checkState(false, 'testing'); + }).should.throw(errors.InvalidState); + }); + it('throws no false negative', function() { + (function() { + $.checkState(true, 'testing'); + }).should.not.throw(); + }); + + it('can be used to check an argument', function() { + (function() { + $.checkArgument(false, 'testing'); + }).should.throw(errors.InvalidArgument); + + (function() { + $.checkArgument(true, 'testing'); + }).should.not.throw(errors.InvalidArgument); + }); + + it('can be used to check an argument type', function() { + var error; + try { + $.checkArgumentType(1, 'string', 'argumentName'); + } catch (e) { + error = e; + e.message.should.equal('Invalid Argument for argumentName, expected string but got number'); + } + error.should.exist(); + }); + it('has no false negatives when used to check an argument type', function() { + (function() { + $.checkArgumentType('a String', 'string', 'argumentName'); + }).should.not.throw(); + }); + + it('can be used to check an argument type for a class', function() { + var error; + try { + $.checkArgumentType(1, PrivateKey); + } catch (e) { + error = e; + e.message.should.equal('Invalid Argument for (unknown name), expected PrivateKey but got number'); + } + error.should.exist(); + }); + it('has no false negatives when checking a type for a class', function() { + (function() { + $.checkArgumentType(new PrivateKey(), PrivateKey); + }).should.not.throw(); + }); +});