Merge pull request #170 from braydonf/coverage

Improve test coverage and cleanup.
This commit is contained in:
Patrick Nagurny 2015-09-02 10:43:41 -04:00
commit fd2790daab
18 changed files with 923 additions and 409 deletions

View File

@ -1,31 +0,0 @@
'use strict';
var BitcoinNode = require('..').Node;
var chainlib = require('chainlib');
var log = chainlib.log;
log.debug = function() {};
var configuration = {
datadir: process.env.BITCORENODE_DIR || '~/.bitcoin',
network: 'testnet'
};
var node = new BitcoinNode(configuration);
var count = 0;
var interval;
node.on('ready', function() {
interval = setInterval(function() {
log.info('Sync Status: Tip:', node.chain.tip.hash, 'Height:', node.chain.tip.__height, 'Rate:', count/10, 'blocks per second');
count = 0;
}, 10000);
});
node.on('error', function(err) {
log.error(err);
});
node.chain.on('addblock', function(block) {
count++;
});

File diff suppressed because one or more lines are too long

View File

@ -1,26 +0,0 @@
{
"unspent": [
{
"txid": "0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098",
"outputIndex": 0
},
{
"txid": "8131ffb0a2c945ecaf9b9063e59558784f9c3a74741ce6ae2a18d0571dac15bb",
"outputIndex": 1
},
{
"txid": "226bbc4b1f851857f37aa96e9eb702946fc128b055e4decc684740005f5044cf",
"outputIndex": 0
}
],
"spent": [
{
"txid": "0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9",
"outputIndex": 0
},
{
"txid": "fff2525b8931402dd09222c50775608f75787bd2b87e56995a7bdd30f79702c4",
"outputIndex": 1
}
]
}

File diff suppressed because one or more lines are too long

View File

@ -1,188 +0,0 @@
'use strict';
// These tests require a fully synced Bitcore Code data directory.
// To run the tests: $ mocha -R spec livenet.js
var chai = require('chai');
var bitcore = require('bitcore');
var bitcoind;
/* jshint unused: false */
var should = chai.should();
var assert = chai.assert;
var sinon = require('sinon');
var txData = require('./livenet-tx-data.json');
var blockData = require('./livenet-block-data.json');
var testTxData = require('./livenet-tx-data.json');
var spentData = require('./livenet-spents.json').spent;
var unspentData = require('./livenet-spents.json').unspent;
var testBlockData = require('./testnet-block-data.json');
describe('Basic Functionality', function() {
before(function(done) {
this.timeout(30000);
bitcoind = require('../').daemon({
datadir: process.env.BITCOINDJS_DIR || '~/.bitcoin',
});
bitcoind.on('error', function(err) {
bitcoind.log('error="%s"', err.message);
});
bitcoind.on('open', function(status) {
bitcoind.log('status="%s"', status);
});
console.log('Waiting for Bitcoin Core to initialize...');
bitcoind.on('ready', function() {
done();
});
});
after(function(done) {
this.timeout(20000);
bitcoind.stop(function(err, result) {
done();
});
});
describe('get transactions by hash', function() {
txData.forEach(function(data) {
var tx = bitcore.Transaction();
tx.fromString(data);
it('for tx ' + tx.hash, function(done) {
bitcoind.getTransaction(tx.hash, true, function(err, response) {
if (err) {
throw err;
}
assert(response.toString('hex') === data, 'incorrect tx data for ' + tx.hash);
done();
});
});
});
});
describe('determine if outpoint is unspent/spent', function() {
spentData.forEach(function(data) {
it('for spent txid ' + data.txid + ' and output ' + data.outputIndex, function() {
var spent = bitcoind.isSpent(data.txid, data.outputIndex, true);
spent.should.equal(true);
});
});
unspentData.forEach(function(data) {
it('for unspent txid ' + data.txid + ' and output ' + data.outputIndex, function() {
var spent = bitcoind.isSpent(data.txid, data.outputIndex, true);
spent.should.equal(false);
});
});
});
describe('get blocks by hash', function() {
blockData.forEach(function(data) {
var block = bitcore.Block.fromString(data);
it('block ' + block.hash, function(done) {
bitcoind.getBlock(block.hash, function(err, response) {
assert(response.toString('hex') === data, 'incorrect block data for ' + block.hash);
done();
});
});
});
});
describe('get blocks by height', function() {
var knownHeights = [
[0, '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f'],
[1, '00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048'],
[100000,'000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506'],
[314159, '00000000000000001bb82a7f5973618cfd3185ba1ded04dd852a653f92a27c45']
];
knownHeights.forEach(function(data) {
it('block at height ' + data[0], function(done) {
bitcoind.getBlock(data[0], function(err, response) {
if (err) {
throw err;
}
var block = bitcore.Block.fromBuffer(response);
block.hash.should.equal(data[1]);
done();
});
});
});
});
describe('get chain work', function() {
it('will get the total work for the genesis block via hash', function() {
var hash = '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f';
var work = bitcoind.getChainWork(hash);
work.should.equal('0000000000000000000000000000000000000000000000000000000100010001');
});
it('will get the total work for block #300000 via hash', function() {
var hash = '000000000000000082ccf8f1557c5d40b21edabb18d2d691cfbf87118bac7254';
var work = bitcoind.getChainWork(hash);
work.should.equal('000000000000000000000000000000000000000000005a7b3c42ea8b844374e9');
});
it('will return undefined for unknown block', function() {
var hash = 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
var work = bitcoind.getChainWork(hash);
should.equal(work, undefined);
});
});
describe('mempool functionality', function() {
var fromAddress = 'mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1';
var utxo = {
address: fromAddress,
txId: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458',
outputIndex: 0,
script: bitcore.Script.buildPublicKeyHashOut(fromAddress).toString(),
satoshis: 100000
};
var toAddress = 'mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc';
var changeAddress = 'mgBCJAsvzgT2qNNeXsoECg2uPKrUsZ76up';
var changeAddressP2SH = '2N7T3TAetJrSCruQ39aNrJvYLhG1LJosujf';
var privateKey = 'cSBnVM4xvxarwGQuAfQFwqDg9k5tErHUHzgWsEfD4zdwUasvqRVY';
var private1 = '6ce7e97e317d2af16c33db0b9270ec047a91bff3eff8558afb5014afb2bb5976';
var private2 = 'c9b26b0f771a0d2dad88a44de90f05f416b3b385ff1d989343005546a0032890';
var tx = new bitcore.Transaction();
tx.from(utxo);
tx.to(toAddress, 50000);
tx.change(changeAddress);
tx.sign(privateKey);
it('will add an unchecked transaction', function() {
var added = bitcoind.addMempoolUncheckedTransaction(tx.serialize());
added.should.equal(true);
bitcoind.getTransaction(tx.hash, true, function(err, txBuffer) {
if(err) {
throw err;
}
var expected = tx.toBuffer().toString('hex');
txBuffer.toString('hex').should.equal(expected);
});
});
it('get outputs by address', function() {
var outputs = bitcoind.getMempoolOutputs(changeAddress);
var expected = [
{
script: 'OP_DUP OP_HASH160 073b7eae2823efa349e3b9155b8a735526463a0f OP_EQUALVERIFY OP_CHECKSIG',
satoshis: 40000,
txid: tx.hash,
outputIndex: 1
}
];
outputs.should.deep.equal(expected);
});
});
});

File diff suppressed because one or more lines are too long

View File

@ -1,5 +0,0 @@
[
"0100000001eccd472c53ec7827eb8952a32d001ca95072ebe0a530f09cdf7219c0ce3f47be010000006a4730440220499cf8379c82301df2295c1581e1d683fec8cc201097efef748fcf3536532882022048c35b24485ec57f387745daf9fc50414ba76e50e914b53b9070f0a4a552f662012102a82fb2ce6e2ea8abb977d9825a2360c9447003dc91da5b8ffa5fc4b3c7808f3fffffffff020001b2c4000000001976a91442f0eb6de4628a6a7ae1afc85b027ff5e0aa25de88ac00437f1f690000001976a91443a3fc9d2ec090a08775957c364073e8b69cfe0688ac00000000",
"01000000021ae3aa69180bbf3d898cd31232224cfe6a2f21ff34051b6fc14f99528e2f0aee010000006b483045022100c9e8656a407fc1fb379b1669abbed3415483bcd20929ebc7fa2f7ecf7c7a7f7b022021dca2336f14df90f201ff8caa8d7a4b8e1760d07afecef880e0cfcbd13756d70121038f4478ad50111c8d04c692fcb6ce560f617a1d01b3056436ebf906d802a422afffffffff1ae3aa69180bbf3d898cd31232224cfe6a2f21ff34051b6fc14f99528e2f0aee030000008a4730440220275f9ab3083c78b368f1e33341275c776fc4af4105ee2646004e9ffb4cba639302202739d0063726a7bcea6681172892c8ab8c1c0b09b1a4d5ed1dc0cf9bea7b18990141048dcb436c4c027e8a5826bdf3c8b6cb326abe66a2d34a18aa473442d8c535f902da45df071964fc8304658b20f9dd1ed241dc48578bf775624a9bd8c52b92c389ffffffff02402b9b00000000001976a914f4f065395c82c6f28d3c73cef113d6da0f90d82a88ac5423f984000000001976a9145af0fe772e1600100b2000f8279f78ce66b7e1d588ac00000000",
"0100000002741453f89d091381329178b3d38cc213c21caede56f5a4a6b74e16ae9bd78b20000000006b483045022100bc0fa3e50ba16589664fd368139c02e3a9da55e20cceed340645b00f6466edac022068cad98f90006534afa2c38551248c6161861e32cb1af6ee1d0f75886263f9c20121038e9dac10e76c6c48a9b791de55f3ea009ee5d10532088f2eb9991d55b03b44e8ffffffff092910ede6e477e9589f8ce00807154378fcbd482d630b2c8cdd10243e74db79010000006b4830450221009cc13ef1acfdfc4535d888b1ea495b3c543e4fabdf38ce8cde4a37171136977c02201a0571e3ab3fd70ca4d97193fcd7999b35760ea4cac404edd1e50a338e50a28d012102e397fa30f882d076e393deec070d423138bd9544aa4cb53450b609713bb327aaffffffff02d5420400000000001976a914171e633a137d5c2275174380c6ca761d7e30365f88acb01a0200000000001976a914263ca88fd638fdb0e2ee8e2a16ec21ecea7a3d7e88ac00000000"
]

View File

@ -12,7 +12,7 @@ function getDefaultConfig() {
config: {
datadir: process.env.BITCORENODE_DIR || path.resolve(process.env.HOME, '.bitcoin'),
network: process.env.BITCORENODE_NETWORK || 'livenet',
port: process.env.BITCORENODE_PORT || 3001,
port: Number(process.env.BITCORENODE_PORT) || 3001,
services: ['bitcoind', 'db', 'address', 'web']
}
};

View File

@ -1,7 +1,6 @@
'use strict';
var path = require('path');
var socketio = require('socket.io');
var BitcoreNode = require('../node');
var index = require('../');
var bitcore = require('bitcore');
@ -10,35 +9,43 @@ var $ = bitcore.util.preconditions;
var log = index.log;
log.debug = function() {};
var count = 0;
var interval = false;
function start(options) {
/* jshint maxstatements: 100 */
/**
* This function will loop over the configuration for services and require the
* specified modules, and assemble an array in this format:
* [
* {
* name: 'bitcoind',
* config: {},
* module: BitcoinService
* }
* ]
* @param {Function} req - The require function to use
* @param {Object} config
* @param {Array} config.services - An array of strings of service names.
* @returns {Array}
*/
function setupServices(req, config) {
var services = [];
var configPath = options.path;
var config = options.config;
if (config.services) {
for (var i = 0; i < config.services.length; i++) {
var service = {};
service.name = config.services[i];
service.config = config.servicesConfig && config.servicesConfig[service.name] ? config.servicesConfig[service.name] : {};
var hasConfig = config.servicesConfig && config.servicesConfig[service.name];
service.config = hasConfig ? config.servicesConfig[service.name] : {};
try {
// first try in the built-in bitcore-node services directory
service.module = require(path.resolve(__dirname, '../services/' + service.name));
service.module = req(path.resolve(__dirname, '../services/' + service.name));
} catch(e) {
// check if the package.json specifies a specific file to use
var servicePackage = require(service.name + '/package.json');
var servicePackage = req(service.name + '/package.json');
var serviceModule = service.name;
if (servicePackage.bitcoreNode) {
serviceModule = service.name + '/' + servicePackage.bitcoreNode;
}
service.module = require(serviceModule);
service.module = req(serviceModule);
}
// check that the service supports expected methods
@ -54,16 +61,18 @@ function start(options) {
services.push(service);
}
}
return services;
}
var fullConfig = _.clone(config);
/**
* Will register event handlers to log the current db sync status.
* @param {Node} node
*/
function registerSyncHandlers(node, delay) {
// expand to the full path
fullConfig.datadir = path.resolve(configPath, config.datadir);
// load the services
fullConfig.services = services;
var node = new BitcoreNode(fullConfig);
delay = delay || 10000;
var interval = false;
var count = 0;
function logSyncStatus() {
log.info(
@ -74,18 +83,7 @@ function start(options) {
}
node.on('synced', function() {
// Stop logging of sync status
clearInterval(interval);
interval = false;
logSyncStatus();
});
node.on('ready', function() {
log.info('Bitcore Node ready');
});
node.on('error', function(err) {
log.error(err);
});
node.on('ready', function() {
@ -96,7 +94,7 @@ function start(options) {
interval = setInterval(function() {
logSyncStatus();
count = 0;
}, 10000);
}, delay);
}
});
});
@ -104,41 +102,88 @@ function start(options) {
node.on('stopping', function() {
clearInterval(interval);
});
}
/**
* Will register event handlers to stop the node for `process` events
* `uncaughtException` and `SIGINT`.
* @param {Object} proc - The Node.js process
* @param {Node} node
*/
function registerExitHandlers(proc, node) {
function exitHandler(options, err) {
if (err) {
log.error('uncaught exception:', err);
if(err.stack) {
console.log(err.stack);
log.error(err.stack);
}
node.stop(function(err) {
if(err) {
log.error('Failed to stop services: ' + err);
}
process.exit(-1);
proc.exit(-1);
});
}
if (options.sigint) {
node.stop(function(err) {
if(err) {
log.error('Failed to stop services: ' + err);
return process.exit(1);
return proc.exit(1);
}
log.info('Halted');
process.exit(0);
proc.exit(0);
});
}
}
//catches uncaught exceptions
process.on('uncaughtException', exitHandler.bind(null, {exit:true}));
proc.on('uncaughtException', exitHandler.bind(null, {exit:true}));
//catches ctrl+c event
process.on('SIGINT', exitHandler.bind(null, {sigint:true}));
proc.on('SIGINT', exitHandler.bind(null, {sigint:true}));
}
/**
* This function will instantiate and start a Node, requiring the necessary service
* modules, and registering event handlers.
* @param {Object} options
* @param {String} options.path - The absolute path of the configuration file
* @param {Object} options.config - The parsed bitcore-node.json configuration file
* @param {Array} options.config.services - An array of services names.
* @param {Object} options.config.servicesConfig - Parameters to pass to each service
* @param {String} options.config.datadir - A relative (to options.path) or absolute path to the datadir
* @param {String} options.config.network - 'livenet', 'testnet' or 'regtest
* @param {Number} options.config.port - The port to use for the web service
*/
function start(options) {
var fullConfig = _.clone(options.config);
fullConfig.services = setupServices(require, options.config);
fullConfig.datadir = path.resolve(options.path, options.config.datadir);
var node = new BitcoreNode(fullConfig);
// set up the event handlers for logging sync information
registerSyncHandlers(node);
// setup handlers for uncaught exceptions and ctrl+c
registerExitHandlers(process, node);
node.on('ready', function() {
log.info('Bitcore Node ready');
});
node.on('error', function(err) {
log.error(err);
});
return node;
}
module.exports = start;
module.exports.registerExitHandlers = registerExitHandlers;
module.exports.registerSyncHandlers = registerSyncHandlers;
module.exports.setupServices = setupServices;

View File

@ -6,6 +6,7 @@ var mkdirp = require('mkdirp');
var fs = require('fs');
var bitcore = require('bitcore');
var $ = bitcore.util.preconditions;
var _ = bitcore.deps._;
var index = require('../');
var log = index.log;
var Service = require('../service');
@ -21,37 +22,14 @@ function Bitcoin(options) {
return new Bitcoin(options);
}
var self = this;
Service.call(this, options);
if (Object.keys(this.instances).length) {
throw new Error('Bitcoin cannot be instantiated more than once.');
}
$.checkState(this.node.datadir, 'Node is missing datadir property');
Object.keys(exports).forEach(function(key) {
self[key] = exports[key];
});
}
util.inherits(Bitcoin, Service);
Bitcoin.dependencies = [];
Bitcoin.instances = {};
Bitcoin.prototype.instances = Bitcoin.instances;
Bitcoin.__defineGetter__('global', function() {
return Bitcoin.instances[Object.keys(Bitcoin.instances)[0]];
});
Bitcoin.prototype.__defineGetter__('global', function() {
return Bitcoin.global;
});
Bitcoin.DEFAULT_CONFIG = 'whitelist=127.0.0.1\n' + 'txindex=1\n';
Bitcoin.prototype._loadConfiguration = function() {
@ -93,16 +71,62 @@ Bitcoin.prototype._loadConfiguration = function() {
);
};
Bitcoin.prototype._onTipUpdate = function(result) {
if (result) {
// Emit and event that the tip was updated
this.height = result;
this.emit('tip', result);
// TODO stopping status
if(!this.node.stopping) {
var percentage = this.syncPercentage();
log.info('Bitcoin Core Daemon New Height:', this.height, 'Percentage:', percentage);
}
// Recursively wait until the next update
bindings.onTipUpdate(this._onTipUpdate.bind(this));
}
};
Bitcoin.prototype._registerEventHandlers = function() {
var self = this;
// Set the height and emit a new tip
bindings.onTipUpdate(self._onTipUpdate.bind(this));
// Register callback function to handle incoming transactions
bindings.startTxMon(function(txs) {
for(var i = 0; i < txs.length; i++) {
self.emit('tx', txs[i]);
}
});
};
Bitcoin.prototype._onReady = function(result, callback) {
var self = this;
self._registerEventHandlers();
var info = self.getInfo();
self.height = info.blocks;
self.getBlock(0, function(err, block) {
if (err) {
return callback(err);
}
self.genesisBuffer = block;
self.emit('ready', result);
log.info('Bitcoin Daemon Ready');
setImmediate(callback);
});
};
Bitcoin.prototype.start = function(callback) {
var self = this;
this._loadConfiguration();
if (this.instances[this.datadir]) {
return callback(new Error('Bitcoin already started'));
}
this.instances[this.datadir] = true;
bindings.start({
datadir: this.node.datadir,
network: this.node.network.name
@ -110,48 +134,12 @@ Bitcoin.prototype.start = function(callback) {
if(err) {
return callback(err);
}
self._started = true;
// Wait until the block chain is ready
bindings.onBlocksReady(function(err, result) {
function onTipUpdateListener(result) {
if (result) {
// Emit and event that the tip was updated
self.height = result;
self.emit('tip', result);
// TODO stopping status
if(!self.stopping) {
var percentage = self.syncPercentage();
log.info('Bitcoin Core Daemon New Height:', self.height, 'Percentage:', percentage);
}
// Recursively wait until the next update
bindings.onTipUpdate(onTipUpdateListener);
}
if (err) {
return callback(err);
}
bindings.onTipUpdate(onTipUpdateListener);
bindings.startTxMon(function(txs) {
for(var i = 0; i < txs.length; i++) {
self.emit('tx', txs[i]);
}
});
// Set the current chain height
var info = self.getInfo();
self.height = info.blocks;
// Get the genesis block
self.getBlock(0, function(err, block) {
self.genesisBuffer = block;
self.emit('ready', result);
log.info('Bitcoin Daemon Ready');
setImmediate(callback);
});
self._onReady(result, callback);
});
});
};

View File

@ -100,8 +100,6 @@ DB.prototype.start = function(callback) {
// Notify that there is a new tip
self.node.services.bitcoind.on('tip', function(height) {
if(!self.node.stopping) {
var percentage = self.node.services.bitcoind.syncPercentage();
log.info('Bitcoin Core Daemon New Height:', height, 'Percentage:', percentage);
self.sync();
}
});
@ -292,19 +290,21 @@ DB.prototype.getPrevHash = function(blockHash, callback) {
/**
* Saves metadata to the database
* @param {Object} metadata - The metadata
* @param {Function} callback - A function that accepts: Error
*/
DB.prototype.putMetadata = function(metadata, callback) {
this.store.put('metadata', JSON.stringify(metadata), {}, callback);
};
DB.prototype.saveMetadata = function(callback) {
var self = this;
callback = callback || function() {};
function defaultCallback(err) {
if (err) {
self.emit('error', err);
}
}
if(self.lastSavedMetadata && Date.now() < self.lastSavedMetadata.getTime() + self.lastSavedMetadataThreshold) {
callback = callback || defaultCallback;
var threshold = self.lastSavedMetadataThreshold;
if (self.lastSavedMetadata && Date.now() < self.lastSavedMetadata.getTime() + threshold) {
return callback();
}
@ -316,7 +316,8 @@ DB.prototype.saveMetadata = function(callback) {
self.lastSavedMetadata = new Date();
self.putMetadata(metadata, callback);
this.store.put('metadata', JSON.stringify(metadata), {}, callback);
};
/**

View File

@ -4,7 +4,6 @@ var async = require('async');
var levelup = require('levelup');
var bitcore = require('bitcore');
var Transaction = bitcore.Transaction;
var BufferWriter = bitcore.encoding.BufferWriter;
Transaction.prototype.populateInputs = function(db, poolTransactions, callback) {
var self = this;
@ -58,13 +57,4 @@ Transaction.prototype._checkSpent = function(db, input, poolTransactions, callba
});
};
Transaction.manyToBuffer = function(transactions) {
var bw = new BufferWriter();
var count = transactions.length;
for(var i = 0; i < count; i++) {
transactions[i].toBufferWriter(bw);
}
return bw.concat();
};
module.exports = Transaction;

View File

@ -36,7 +36,7 @@
"upload": "node bin/upload.js",
"start": "node bin/start.js",
"test": "NODE_ENV=test mocha -R spec --recursive",
"coverage": "istanbul cover _mocha -- --recursive",
"coverage": "NODE_ENV=test istanbul cover _mocha -- --recursive",
"libbitcoind": "node bin/start-libbitcoind.js"
},
"tags": [

View File

@ -0,0 +1,32 @@
'use strict';
var should = require('chai').should();
var defaultConfig = require('../../lib/scaffold/default-config');
describe('#defaultConfig', function() {
it('will return expected configuration', function() {
var cwd = process.cwd();
var home = process.env.HOME;
var info = defaultConfig();
info.path.should.equal(cwd);
info.config.datadir.should.equal(home + '/.bitcoin');
info.config.network.should.equal('livenet');
info.config.port.should.equal(3001);
info.config.services.should.deep.equal(['bitcoind', 'db', 'address', 'web']);
});
it('will return expected configuration from environment variables', function() {
var cwd = process.cwd();
process.env.BITCORENODE_DIR = '/home/bitcore-node/.bitcoin';
process.env.BITCORENODE_NETWORK = 'testnet';
process.env.BITCORENODE_PORT = 3002;
var info = defaultConfig();
info.path.should.equal(cwd);
info.config.datadir.should.equal('/home/bitcore-node/.bitcoin');
info.config.network.should.equal('testnet');
info.config.port.should.equal(3002);
info.config.services.should.deep.equal(['bitcoind', 'db', 'address', 'web']);
});
});

165
test/scaffold/start.unit.js Normal file
View File

@ -0,0 +1,165 @@
'use strict';
var should = require('chai').should();
var EventEmitter = require('events').EventEmitter;
var path = require('path');
var sinon = require('sinon');
var proxyquire = require('proxyquire');
var start = require('../../lib/scaffold/start');
describe('#start', function() {
describe('#setupServices', function() {
var setupServices = proxyquire('../../lib/scaffold/start', {}).setupServices;
it('will require an internal module', function() {
function InternalService() {}
InternalService.dependencies = [];
InternalService.prototype.start = sinon.stub();
InternalService.prototype.stop = sinon.stub();
var expectedPath = path.resolve(__dirname, '../../lib/services/internal');
var testRequire = function(p) {
p.should.equal(expectedPath);
return InternalService;
};
var config = {
services: ['internal'],
servicesConfig: {
internal: {
param: 'value'
}
}
};
var services = setupServices(testRequire, config);
services[0].name.should.equal('internal');
services[0].config.should.deep.equal({param: 'value'});
services[0].module.should.equal(InternalService);
});
it('will require a local module', function() {
function LocalService() {}
LocalService.dependencies = [];
LocalService.prototype.start = sinon.stub();
LocalService.prototype.stop = sinon.stub();
var notfoundPath = path.resolve(__dirname, '../../lib/services/local');
var testRequire = function(p) {
if (p === notfoundPath) {
throw new Error();
} else if (p === 'local') {
return LocalService;
} else if (p === 'local/package.json') {
return {
name: 'local'
};
}
};
var config = {
services: ['local']
};
var services = setupServices(testRequire, config);
services[0].name.should.equal('local');
services[0].module.should.equal(LocalService);
});
it('will require a local module with "bitcoreNode" in package.json', function() {
function LocalService() {}
LocalService.dependencies = [];
LocalService.prototype.start = sinon.stub();
LocalService.prototype.stop = sinon.stub();
var notfoundPath = path.resolve(__dirname, '../../lib/services/local');
var testRequire = function(p) {
if (p === notfoundPath) {
throw new Error();
} else if (p === 'local/package.json') {
return {
name: 'local',
bitcoreNode: 'lib/bitcoreNode.js'
};
} else if (p === 'local/lib/bitcoreNode.js') {
return LocalService;
}
};
var config = {
services: ['local']
};
var services = setupServices(testRequire, config);
services[0].name.should.equal('local');
services[0].module.should.equal(LocalService);
});
it('will throw error if module is incompatible', function() {
var internal = {};
var testRequire = function() {
return internal;
};
var config = {
services: ['bitcoind']
};
(function() {
setupServices(testRequire, config);
}).should.throw('Could not load service');
});
});
describe('#registerSyncHandlers', function() {
it('will log the sync status at an interval', function(done) {
var log = {
info: sinon.stub()
};
var registerSyncHandlers = proxyquire('../../lib/scaffold/start', {
'../': {
log: log
}
}).registerSyncHandlers;
var node = new EventEmitter();
node.services = {
db: new EventEmitter()
};
node.services.db.tip = {
hash: 'hash',
__height: 10
};
registerSyncHandlers(node, 10);
node.emit('ready');
node.services.db.emit('addblock');
setTimeout(function() {
node.emit('synced');
log.info.callCount.should.be.within(3, 4);
done();
}, 35);
});
});
describe('#registerExitHandlers', function() {
var log = {
info: sinon.stub(),
error: sinon.stub()
};
var registerExitHandlers = proxyquire('../../lib/scaffold/start', {
'../': {
log: log
}
}).registerExitHandlers;
it('log, stop and exit with an `uncaughtException`', function(done) {
var proc = new EventEmitter();
proc.exit = sinon.stub();
var node = {
stop: sinon.stub().callsArg(0)
};
registerExitHandlers(proc, node);
proc.emit('uncaughtException', new Error('test'));
setImmediate(function() {
node.stop.callCount.should.equal(1);
proc.exit.callCount.should.equal(1);
done();
});
});
it('stop and exit on `SIGINT`', function(done) {
var proc = new EventEmitter();
proc.exit = sinon.stub();
var node = {
stop: sinon.stub().callsArg(0)
};
registerExitHandlers(proc, node);
proc.emit('SIGINT');
setImmediate(function() {
node.stop.callCount.should.equal(1);
proc.exit.callCount.should.equal(1);
done();
});
});
});
});

View File

@ -1,17 +1,14 @@
'use strict';
var should = require('chai').should();
var sinon = require('sinon');
var proxyquire = require('proxyquire');
var fs = require('fs');
var sinon = require('sinon');
var readFileSync = sinon.stub().returns(fs.readFileSync(__dirname + '/../data/bitcoin.conf'));
var BitcoinService = proxyquire('../../lib/services/bitcoind', {
fs: {
readFileSync: sinon.stub().returns(fs.readFileSync(__dirname + '/../data/bitcoin.conf'))
}
});
var BadBitcoin = proxyquire('../../lib/services/bitcoind', {
fs: {
readFileSync: sinon.stub().returns(fs.readFileSync(__dirname + '/../data/badbitcoin.conf'))
readFileSync: readFileSync
}
});
@ -24,9 +21,33 @@ describe('Bitcoin Service', function() {
}
}
};
describe('@constructor', function() {
it('will create an instance', function() {
var bitcoind = new BitcoinService(baseConfig);
should.exist(bitcoind);
});
it('will create an instance without `new`', function() {
var bitcoind = BitcoinService(baseConfig);
should.exist(bitcoind);
});
});
describe('@dependencies', function() {
it('will have no dependencies', function() {
BitcoinService.dependencies.should.deep.equal([]);
});
});
describe('#_loadConfiguration', function() {
it('will parse a bitcoin.conf file', function() {
var bitcoind = new BitcoinService(baseConfig);
var TestBitcoin = proxyquire('../../lib/services/bitcoind', {
fs: {
readFileSync: readFileSync,
existsSync: sinon.stub().returns(true)
},
mkdirp: {
sync: sinon.stub()
}
});
var bitcoind = new TestBitcoin(baseConfig);
bitcoind._loadConfiguration({datadir: process.env.HOME + '/.bitcoin'});
should.exist(bitcoind.configuration);
bitcoind.configuration.should.deep.equal({
@ -40,11 +61,283 @@ describe('Bitcoin Service', function() {
});
});
it('should throw an exception if txindex isn\'t enabled in the configuration', function() {
var bitcoind = new BadBitcoin(baseConfig);
var TestBitcoin = proxyquire('../../lib/services/bitcoind', {
fs: {
readFileSync: sinon.stub().returns(fs.readFileSync(__dirname + '/../data/badbitcoin.conf')),
existsSync: sinon.stub().returns(true),
},
mkdirp: {
sync: sinon.stub()
}
});
var bitcoind = new TestBitcoin(baseConfig);
(function() {
bitcoind._loadConfiguration({datadir: './test'});
}).should.throw('Txindex option');
});
});
});
describe('#_registerEventHandlers', function() {
it('will emit tx with transactions from bindings', function(done) {
var transaction = {};
var TestBitcoin = proxyquire('../../lib/services/bitcoind', {
fs: {
readFileSync: readFileSync
},
bindings: function(name) {
name.should.equal('bitcoind.node');
return {
onTipUpdate: sinon.stub(),
startTxMon: sinon.stub().callsArgWith(0, [transaction])
};
}
});
var bitcoind = new TestBitcoin(baseConfig);
bitcoind.on('tx', function(tx) {
tx.should.equal(transaction);
done();
});
bitcoind._registerEventHandlers();
});
it('will emit tip from bindings', function(done) {
var height = 1;
var TestBitcoin = proxyquire('../../lib/services/bitcoind', {
fs: {
readFileSync: readFileSync
},
bindings: function(name) {
name.should.equal('bitcoind.node');
return {
syncPercentage: function() {
return height * 10;
},
onTipUpdate: function(callback) {
if (height >= 10) {
return callback(undefined);
}
setImmediate(function() {
callback(height++);
});
},
startTxMon: sinon.stub()
};
}
});
var bitcoind = new TestBitcoin(baseConfig);
var tipCallCount = 0;
bitcoind.on('tip', function(height) {
should.exist(height);
tipCallCount++;
if (height === 9) {
tipCallCount.should.equal(9);
done();
}
});
bitcoind._registerEventHandlers();
});
});
describe('#_onReady', function(done) {
var genesisBuffer = new Buffer('0100000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000bac8b0fa927c0ac8234287e33c5f74d38d354820e24756ad709d7038fc5f31f020e7494dffff001d03e4b6720101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0420e7494d017f062f503253482fffffffff0100f2052a010000002321021aeaf2f8638a129a3156fbe7e5ef635226b0bafd495ff03afe2c843d7e3a4b51ac00000000', 'hex');
it('will emit ready and set the height and genesisBuffer', function(done) {
var TestBitcoin = proxyquire('../../lib/services/bitcoind', {
fs: {
readFileSync: readFileSync
},
bindings: function(name) {
name.should.equal('bitcoind.node');
return {
onTipUpdate: sinon.stub(),
startTxMon: sinon.stub(),
getInfo: sinon.stub().returns({
blocks: 101
}),
getBlock: sinon.stub().callsArgWith(1, null, genesisBuffer)
};
}
});
var bitcoind = new TestBitcoin(baseConfig);
bitcoind._registerEventHandlers = sinon.stub();
var result = {};
var readyCallCount = 0;
bitcoind.on('ready', function() {
readyCallCount++;
});
bitcoind._onReady(result, function(err) {
if (err) {
throw err;
}
bitcoind._registerEventHandlers.callCount.should.equal(1);
readyCallCount.should.equal(1);
bitcoind.genesisBuffer.should.equal(genesisBuffer);
bitcoind.height.should.equal(101);
done();
});
});
});
describe('#start', function() {
it('call bindings start with the correct arguments', function(done) {
var startCallCount = 0;
var start = function(obj, cb) {
startCallCount++;
obj.datadir.should.equal('testdir');
obj.network.should.equal('regtest');
cb();
};
var onBlocksReady = sinon.stub().callsArg(0);
var TestBitcoin = proxyquire('../../lib/services/bitcoind', {
fs: {
readFileSync: readFileSync
},
bindings: function(name) {
name.should.equal('bitcoind.node');
return {
start: start,
onBlocksReady: onBlocksReady
};
}
});
var bitcoind = new TestBitcoin(baseConfig);
bitcoind._loadConfiguration = sinon.stub();
bitcoind._onReady = sinon.stub().callsArg(1);
bitcoind.start(function(err) {
should.not.exist(err);
bitcoind._loadConfiguration.callCount.should.equal(1);
startCallCount.should.equal(1);
onBlocksReady.callCount.should.equal(1);
bitcoind._onReady.callCount.should.equal(1);
done();
});
});
it('will give an error from bindings.start', function(done) {
var start = sinon.stub().callsArgWith(1, new Error('test'));
var TestBitcoin = proxyquire('../../lib/services/bitcoind', {
fs: {
readFileSync: readFileSync
},
bindings: function(name) {
name.should.equal('bitcoind.node');
return {
start: start
};
}
});
var bitcoind = new TestBitcoin(baseConfig);
bitcoind._loadConfiguration = sinon.stub();
bitcoind.start(function(err) {
should.exist(err);
err.message.should.equal('test');
done();
});
});
it('will give an error from bindings.onBlocksReady', function(done) {
var start = sinon.stub().callsArgWith(1, null);
var onBlocksReady = sinon.stub().callsArgWith(0, new Error('test'));
var TestBitcoin = proxyquire('../../lib/services/bitcoind', {
fs: {
readFileSync: readFileSync
},
bindings: function(name) {
name.should.equal('bitcoind.node');
return {
start: start,
onBlocksReady: onBlocksReady
};
}
});
var bitcoind = new TestBitcoin(baseConfig);
bitcoind._onReady = sinon.stub().callsArg(1);
bitcoind._loadConfiguration = sinon.stub();
bitcoind.start(function(err) {
should.exist(err);
err.message.should.equal('test');
done();
});
});
});
describe('#stop', function() {
it('will call bindings stop', function() {
var stop = sinon.stub().callsArgWith(0, null, 'status');
var TestBitcoin = proxyquire('../../lib/services/bitcoind', {
fs: {
readFileSync: readFileSync
},
bindings: function(name) {
name.should.equal('bitcoind.node');
return {
stop: stop
};
}
});
var bitcoind = new TestBitcoin(baseConfig);
bitcoind.stop(function(err, status) {
stop.callCount.should.equal(1);
should.not.exist(err);
});
});
it('will give an error from bindings stop', function() {
var stop = sinon.stub().callsArgWith(0, new Error('test'));
var TestBitcoin = proxyquire('../../lib/services/bitcoind', {
fs: {
readFileSync: readFileSync
},
bindings: function(name) {
name.should.equal('bitcoind.node');
return {
stop: stop
};
}
});
var bitcoind = new TestBitcoin(baseConfig);
bitcoind.stop(function(err) {
stop.callCount.should.equal(1);
should.exist(err);
err.message.should.equal('test');
});
});
});
describe('proxy methods', function() {
var proxyMethods = [
['isSynced', 0],
['syncPercentage', 0],
['getBlock', 2],
['isSpent', 2],
['getBlockIndex', 1],
['estimateFee', 1],
['sendTransaction', 2],
['getTransaction', 3],
['getTransactionWithBlockInfo', 3],
['getMempoolOutputs', 1],
['addMempoolUncheckedTransaction', 1],
['getInfo', 0]
];
proxyMethods.forEach(function(x) {
it('pass ' + x[1] + ' argument(s) to ' + x[0], function() {
var stub = sinon.stub();
var TestBitcoin = proxyquire('../../lib/services/bitcoind', {
fs: {
readFileSync: readFileSync
},
bindings: function(name) {
name.should.equal('bitcoind.node');
var methods = {};
methods[x[0]] = stub;
return methods;
}
});
var bitcoind = new TestBitcoin(baseConfig);
var args = [];
for (var i = 0; i < x[1]; i++) {
args.push(i);
}
bitcoind[x[0]].apply(bitcoind, args);
stub.callCount.should.equal(1);
stub.args[0].length.should.equal(x[1]);
});
});
});
});

View File

@ -16,6 +16,7 @@ var chainHashes = require('../data/hashes.json');
var chainData = require('../data/testnet-blocks.json');
var errors = index.errors;
var memdown = require('memdown');
var levelup = require('levelup');
var bitcore = require('bitcore');
var Transaction = bitcore.Transaction;
@ -237,7 +238,6 @@ describe('DB Service', function() {
var db = new TestDB(baseConfig);
db.node.services = {};
db.node.services.bitcoind = new EventEmitter();
db.node.services.bitcoind.syncPercentage = sinon.spy();
db.node.services.bitcoind.genesisBuffer = genesisBuffer;
db.getMetadata = sinon.stub().callsArg(0);
db.connectBlock = sinon.stub().callsArg(1);
@ -245,7 +245,6 @@ describe('DB Service', function() {
db.sync = sinon.stub();
db.start(function() {
db.sync = function() {
db.node.services.bitcoind.syncPercentage.callCount.should.equal(1);
done();
};
db.node.services.bitcoind.emit('tip', 10);
@ -466,7 +465,7 @@ describe('DB Service', function() {
});
});
describe("#estimateFee", function() {
describe('#estimateFee', function() {
it('should pass along the fee from bitcoind', function(done) {
var db = new DB(baseConfig);
db.node = {};
@ -484,6 +483,140 @@ describe('DB Service', function() {
});
});
describe('#saveMetadata', function() {
it('will emit an error with default callback', function(done) {
var db = new DB(baseConfig);
db.cache = {
hashes: {},
chainHashes: {}
};
db.tip = {
hash: '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f',
__height: 0
};
db.store = {
put: sinon.stub().callsArgWith(3, new Error('test'))
};
db.on('error', function(err) {
err.message.should.equal('test');
done();
});
db.saveMetadata();
});
it('will give an error with callback', function(done) {
var db = new DB(baseConfig);
db.cache = {
hashes: {},
chainHashes: {}
};
db.tip = {
hash: '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f',
__height: 0
};
db.store = {
put: sinon.stub().callsArgWith(3, new Error('test'))
};
db.saveMetadata(function(err) {
err.message.should.equal('test');
done();
});
});
it('will call store with the correct arguments', function(done) {
var db = new DB(baseConfig);
db.cache = {
hashes: {},
chainHashes: {}
};
db.tip = {
hash: '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f',
__height: 0
};
db.store = {
put: function(key, value, options, callback) {
key.should.equal('metadata');
JSON.parse(value).should.deep.equal({
tip: '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f',
tipHeight: 0,
cache: {
hashes: {},
chainHashes: {}
}
});
options.should.deep.equal({});
callback.should.be.a('function');
done();
}
};
db.saveMetadata();
});
it('will not call store with threshold', function(done) {
var db = new DB(baseConfig);
db.lastSavedMetadata = new Date();
db.lastSavedMetadataThreshold = 30000;
var put = sinon.stub();
db.store = {
put: put
};
db.saveMetadata(function(err) {
if (err) {
throw err;
}
put.callCount.should.equal(0);
done();
});
});
});
describe('#getMetadata', function() {
it('will get metadata', function() {
var db = new DB(baseConfig);
var json = JSON.stringify({
tip: '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f',
tipHeight: 101,
cache: {
hashes: {},
chainHashes: {}
}
});
db.store = {};
db.store.get = sinon.stub().callsArgWith(2, null, json);
db.getMetadata(function(err, data) {
data.tip.should.equal('000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f');
data.tipHeight.should.equal(101);
data.cache.should.deep.equal({
hashes: {},
chainHashes: {}
});
});
});
it('will handle a notfound error from leveldb', function() {
var db = new DB(baseConfig);
db.store = {};
var error = new levelup.errors.NotFoundError();
db.store.get = sinon.stub().callsArgWith(2, error);
db.getMetadata(function(err, data) {
should.not.exist(err);
data.should.deep.equal({});
});
});
it('will handle error from leveldb', function() {
var db = new DB(baseConfig);
db.store = {};
db.store.get = sinon.stub().callsArgWith(2, new Error('test'));
db.getMetadata(function(err) {
err.message.should.equal('test');
});
});
it('give an error when parsing invalid json', function() {
var db = new DB(baseConfig);
db.store = {};
db.store.get = sinon.stub().callsArgWith(2, null, '{notvalid@json}');
db.getMetadata(function(err) {
err.message.should.equal('Could not parse metadata');
});
});
});
describe('#connectBlock', function() {
it('should remove block from mempool and call blockHandler with true', function(done) {
var db = new DB(baseConfig);

133
test/utils.unit.js Normal file
View File

@ -0,0 +1,133 @@
'use strict';
var should = require('chai').should();
var utils = require('../lib/utils');
describe('Utils', function() {
describe('#isHash', function() {
it('false for short string', function() {
var a = utils.isHash('ashortstring');
a.should.equal(false);
});
it('false for long string', function() {
var a = utils.isHash('00000000000000000000000000000000000000000000000000000000000000000');
a.should.equal(false);
});
it('false for correct length invalid char', function() {
var a = utils.isHash('z000000000000000000000000000000000000000000000000000000000000000');
a.should.equal(false);
});
it('false for invalid type (buffer)', function() {
var a = utils.isHash(new Buffer('abcdef', 'hex'));
a.should.equal(false);
});
it('false for invalid type (number)', function() {
var a = utils.isHash(123456);
a.should.equal(false);
});
it('true for hash', function() {
var a = utils.isHash('fc63629e2106c3440d7e56751adc8cfa5266a5920c1b54b81565af25aec1998b');
a.should.equal(true);
});
});
describe('#isSafeNatural', function() {
it('false for float', function() {
var a = utils.isSafeNatural(0.1);
a.should.equal(false);
});
it('false for string float', function() {
var a = utils.isSafeNatural('0.1');
a.should.equal(false);
});
it('false for string integer', function() {
var a = utils.isSafeNatural('1');
a.should.equal(false);
});
it('false for negative integer', function() {
var a = utils.isSafeNatural(-1);
a.should.equal(false);
});
it('false for negative integer string', function() {
var a = utils.isSafeNatural('-1');
a.should.equal(false);
});
it('false for infinity', function() {
var a = utils.isSafeNatural(Infinity);
a.should.equal(false);
});
it('false for NaN', function() {
var a = utils.isSafeNatural(NaN);
a.should.equal(false);
});
it('false for unsafe number', function() {
var a = utils.isSafeNatural(Math.pow(2, 53));
a.should.equal(false);
});
it('true for positive integer', function() {
var a = utils.isSafeNatural(1000);
a.should.equal(true);
});
});
describe('#startAtZero', function() {
it('will set key to zero if not set', function() {
var obj = {};
utils.startAtZero(obj, 'key');
obj.key.should.equal(0);
});
it('not if already set', function() {
var obj = {
key: 10
};
utils.startAtZero(obj, 'key');
obj.key.should.equal(10);
});
it('not if set to false', function() {
var obj = {
key: false
};
utils.startAtZero(obj, 'key');
obj.key.should.equal(false);
});
it('not if set to undefined', function() {
var obj = {
key: undefined
};
utils.startAtZero(obj, 'key');
should.equal(obj.key, undefined);
});
it('not if set to null', function() {
var obj = {
key: null
};
utils.startAtZero(obj, 'key');
should.equal(obj.key, null);
});
});
});