Merge pull request #24 from maraoz/tdd/api

add tests for REST API with mocked services
This commit is contained in:
Esteban Ordano 2015-03-10 12:57:55 -03:00
commit d704a6d143
15 changed files with 616 additions and 25 deletions

102
api/controllers/blocks.js Normal file
View File

@ -0,0 +1,102 @@
'use strict';
var bitcore = require('bitcore');
var _ = bitcore.deps._;
var $ = bitcore.util.preconditions;
var Block = bitcore.Block;
var BitcoreNode = require('../../');
var Blocks = {};
var node;
Blocks.setNode = function(aNode) {
node = aNode;
};
/*
* params
*/
/*
* Finds a block by its hash
*/
Blocks.blockHashParam = function(req, res, next, blockHash) {
node.getBlock(blockHash)
.then(function(block) {
req.block = block;
})
.then(next)
.catch(BitcoreNode.errors.Blocks.NotFound, function() {
res.status(404).send('Block with id ' + blockHash + ' not found');
});
};
/*
* Finds a block by its height
*/
Blocks.heightParam = function(req, res, next, height) {
height = parseInt(height);
node.getBlock(height)
.then(function(block) {
req.block = block;
})
.then(next)
.catch(BitcoreNode.errors.Blocks.NotFound, function() {
res.status(404).send('Block with height ' + height + ' not found');
});
};
/*
* controllers
*/
/*
* Returns a list of blocks given certain query options.
*
* from: block height as lower limit (default: 0)
* to: ditto, but for the upper limit, non inclusive (default: 1000000)
* page: for paginating results (default: 0)
* offset: skip the first offset blocks (default: 0)
* limit: max amount of blocks returned (default: 10)
*
*/
Blocks.list = function(req, res) {
var from = parseInt(req.query.from || 0);
var to = parseInt(req.query.to || 1e6);
var offset = parseInt(req.query.offset || 0);
var limit = parseInt(req.query.limit || 10);
if (to < from) {
res.status(422);
res.send('/v1/blocks/ "to" must be >= to "from"');
return;
}
node.listBlocks(from, to, offset, limit)
.then(function(blocks) {
res.send(blocks);
});
};
Blocks.getLatest = function(req, res) {
node.getLatestBlock()
.then(function(block) {
req.block = block;
Blocks.get(req, res);
});
};
Blocks.get = function(req, res) {
$.checkState(req.block instanceof Block);
res.send(req.block.toObject());
};
Blocks.getBlockError = function(req, res) {
res.status(422);
res.send('/v1/blocks/ parameter must be a 64 digit hex or block height integer');
};
module.exports = Blocks;

16
api/controllers/node.js Normal file
View File

@ -0,0 +1,16 @@
'use strict';
var NodeStatus = {};
var node;
NodeStatus.setNode = function(aNode) {
node = aNode;
};
NodeStatus.getStatus = function(req, res) {
node.getStatus()
.then(function(status) {
res.send(status);
});
};
module.exports = NodeStatus;

View File

@ -0,0 +1,88 @@
'use strict';
var Promise = require('bluebird');
var bitcore = require('bitcore');
var _ = bitcore.deps._;
var $ = bitcore.util.preconditions;
var Transaction = bitcore.Transaction;
var BitcoreNode = require('../../');
var Transactions = {};
var node;
Transactions.setNode = function(aNode) {
node = aNode;
};
/*
* params
*/
/*
* Finds a transaction by its hash
*/
Transactions.txHashParam = function(req, res, next, txHash) {
node.getTransaction(txHash)
.then(function(tx) {
req.tx = tx;
})
.then(next)
.catch(BitcoreNode.errors.Transactions.NotFound, function() {
res.status(404).send('Transaction with id ' + txHash + ' not found');
});
};
/*
* controllers
*/
/*
* get transaction by its hash
*/
Transactions.get = function(req, res) {
$.checkState(req.tx instanceof Transaction);
res.send(req.tx.toObject());
};
/**
* send a transaction to the bitcoin network
*/
Transactions.send = function(req, res) {
var raw = req.body.raw;
if (_.isUndefined(raw)) {
Transaction._sendError(res);
return;
}
var tx;
try {
tx = new Transaction(raw);
} catch (e) {
Transaction._sendError(res);
return;
}
node.broadcast(tx)
.then(function() {
res.send('Transaction broadcasted successfully');
})
.catch(BitcoreNode.errors.Transactions.CantBroadcast, function(err) {
res.status(422).send(err.message);
});
};
Transaction._sendError = function(res) {
res.status(422);
res.send('/v1/transactions/send parameter must be a raw transaction hex');
};
Transactions.getTxError = function(req, res) {
res.status(422);
res.send('/v1/transactions/ parameter must be a 64 digit hex');
};
module.exports = Transactions;

View File

@ -1,8 +1,11 @@
'use strict';
var config = require('config');
var BitcoreHTTP = require('./lib/http');
var http = BitcoreHTTP.create(config.get('BitcoreHTTP'));
http.start();
if (require.main === module) {
var config = require('config');
var http = BitcoreHTTP.create(config.get('BitcoreHTTP'));
http.start();
}
module.exports = BitcoreHTTP;

View File

@ -1,31 +1,42 @@
'use strict';
var express = require('express');
var NodeStatus = require('../controllers/node');
var Blocks = require('../controllers/blocks');
var Transactions = require('../controllers/transactions');
function initRouter(node) {
var router = express.Router();
[NodeStatus, Blocks, Transactions].forEach(function(controller) {
controller.setNode(node);
});
function mockResponse(req, res) {
res.send({'message': 'This is a mocked response'});
res.send({
'message': 'This is a mocked response'
});
}
// parameter middleware
router.param('blockHash', Blocks.blockHashParam);
router.param('height', Blocks.heightParam);
router.param('txHash', Transactions.txHashParam);
// Node routes
router.get('/node', mockResponse);
router.get('/node', NodeStatus.getStatus);
// Block routes
router.get('/blocks', mockResponse);
router.get('/blocks/latest', mockResponse);
router.get('/blocks/:blockHash', mockResponse);
router.get('/blocks/:height', mockResponse);
router.get('/blocks/:blockHash/transactions/:txIndex', mockResponse);
router.get('/blocks', Blocks.list);
router.get('/blocks/latest', Blocks.getLatest);
router.get('/blocks/:blockHash([A-Fa-f0-9]{64})', Blocks.get);
router.get('/blocks/:height([0-9]+)', Blocks.get);
// Transaction routes
router.get('/transactions', mockResponse);
router.get('/transactions/:txHash', mockResponse);
router.post('/transactions/send', mockResponse);
router.get('/transactions/:txHash/addresses', mockResponse);
router.get('/transactions/:txHash/outputs/addresses', mockResponse);
router.get('/transactions/:txHash/inputs/addresses', mockResponse);
router.get('/transactions/:txHash([A-Fa-f0-9]{64})', Transactions.get);
router.post('/transactions/send', Transactions.send);
// Input routes
router.get('/transactions/:txHash/inputs', mockResponse);
@ -42,6 +53,10 @@ function initRouter(node) {
// TODO: check if this is really restful
router.get('/addresses/:addresses/utxos', mockResponse);
// error routes
router.get('/blocks/*', Blocks.getBlockError);
router.get('/transactions/*', Transactions.getTxError);
return router;
}

15
api/test/data/blocks.js Normal file
View File

@ -0,0 +1,15 @@
'use strict';
var bitcore = require('bitcore');
var Block = bitcore.Block;
var mockBlocks = {};
var blockHexs = require('./blocks.json');
blockHexs.map(function(hex) {
var block = new Block(new Buffer(hex, 'hex'));
return block;
}).forEach(function(block) {
mockBlocks[block.id] = block;
});
module.exports = mockBlocks;

12
api/test/data/blocks.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,17 @@
'use strict';
var bitcore = require('bitcore');
var Block = bitcore.Block;
var mockTransactions = {};
var blockHexs = require('./blocks.json');
blockHexs.map(function(hex) {
var block = new Block(new Buffer(hex, 'hex'));
return block;
}).forEach(function(block) {
block.transactions.forEach(function(tx) {
mockTransactions[tx.id] = tx;
});
});
module.exports = mockTransactions;

View File

@ -10,6 +10,9 @@ var BitcoreHTTP = require('../lib/http');
describe('BitcoreHTTP', function() {
// mocks
var opts = {
port: 1234
};
var nodeMock;
beforeEach(function() {
nodeMock = new EventEmitter();
@ -25,7 +28,7 @@ describe('BitcoreHTTP', function() {
});
});
it('starts', function() {
var http = new BitcoreHTTP(nodeMock);
var http = new BitcoreHTTP(nodeMock, opts);
http.start.bind(http).should.not.throw();
});

View File

@ -29,12 +29,5 @@ describe('BitcoreHTTP routes', function() {
.expect(200)
.expect('bitcore-node API', cb);
});
it('blocks', function(cb) {
agent.get('/v1/blocks/')
.expect(200)
.expect({
'message': 'This is a mocked response'
}, cb);
});
});

147
api/test/v1/blocks.js Normal file
View File

@ -0,0 +1,147 @@
'use strict';
var chai = require('chai');
var should = chai.should();
var request = require('supertest');
var EventEmitter = require('eventemitter2').EventEmitter2;
var Promise = require('bluebird');
Promise.longStackTraces();
var bitcore = require('bitcore');
var _ = bitcore.deps._;
var BitcoreHTTP = require('../../lib/http');
var BitcoreNode = require('../../../');
var mockBlocks = require('../data/blocks');
Object.values = function(obj) {
var vals = [];
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
vals.push(obj[key]);
}
}
return vals;
};
describe('BitcoreHTTP v1 blocks routes', function() {
// mocks
var b1 = mockBlocks[Object.keys(mockBlocks)[0]];
var firstBlock = mockBlocks[Object.keys(mockBlocks).splice(0, 1)[0]];
var secondBlock = mockBlocks[Object.keys(mockBlocks).splice(1, 1)[0]];
var lastBlock = mockBlocks[Object.keys(mockBlocks).splice(-1)[0]];
var blockForHash = function(hash) {
return mockBlocks[hash];
};
var last3 = Object.keys(mockBlocks).splice(-3).map(blockForHash);
var some2 = Object.keys(mockBlocks).splice(2,2).map(blockForHash);
var nodeMock, app, agent;
var blockList = Object.values(mockBlocks);
beforeEach(function() {
nodeMock = new EventEmitter();
nodeMock.getBlock = function(blockHash) {
var block;
if (typeof blockHash === 'number') {
var height = blockHash;
block = mockBlocks[Object.keys(mockBlocks)[height - 100000]];
} else {
block = mockBlocks[blockHash];
}
if (_.isUndefined(block)) {
return Promise.reject(new BitcoreNode.errors.Blocks.NotFound(blockHash));
}
return Promise.resolve(block);
};
nodeMock.getLatestBlock = function() {
return Promise.resolve(lastBlock);
};
nodeMock.listBlocks = function(from, to, offset, limit) {
var start = from - 1e5;
var end = to - 1e5;
var section = blockList.slice(start, end);
return Promise.resolve(section.slice(offset, offset + limit));
};
app = new BitcoreHTTP(nodeMock).app;
agent = request(app);
});
describe('/blocks', function() {
it('works with default parameters', function(cb) {
agent.get('/v1/blocks/')
.expect(200)
.expect(JSON.stringify(blockList), cb);
});
it('fails with to<from', function(cb) {
agent.get('/v1/blocks/?from=100000&to=99999')
.expect(422)
.expect('/v1/blocks/ "to" must be >= to "from"', cb);
});
it('works with to/from parameters', function(cb) {
agent.get('/v1/blocks/?from=100000&to=100001')
.expect(200)
.expect(JSON.stringify([firstBlock]), cb);
});
it('works with limit/offset parameters', function(cb) {
agent.get('/v1/blocks/?limit=1&offset=1')
.expect(200)
.expect(JSON.stringify([secondBlock]), cb);
});
it('works with all parameters', function(cb) {
agent.get('/v1/blocks/?from=100005&to=100020&limit=3&offset=2')
.expect(200)
.expect(JSON.stringify(last3), cb);
});
it('works with all parameters 2', function(cb) {
agent.get('/v1/blocks/?from=100000&to=100005&limit=2&offset=2')
.expect(200)
.expect(JSON.stringify(some2), cb);
});
});
describe('/blocks/latest', function() {
it('returns latest block', function(cb) {
agent.get('/v1/blocks/latest')
.expect(200)
.expect(lastBlock.toJSON(), cb);
});
});
describe('/blocks/:blockHash', function() {
it('fails with invalid blockHash', function(cb) {
agent.get('/v1/blocks/abad1dea')
.expect(422)
.expect('/v1/blocks/ parameter must be a 64 digit hex or block height integer', cb);
});
it('returns 404 with non existent block', function(cb) {
agent.get('/v1/blocks/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b600000000')
.expect(404)
.expect('Block with id 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b600000000 not found', cb);
});
Object.keys(mockBlocks).forEach(function(hash) {
var block = mockBlocks[hash];
it('works with valid blockHash ...' + hash.substring(hash.length - 8), function(cb) {
agent.get('/v1/blocks/' + hash)
.expect(200)
.expect(block.toJSON(), cb);
});
});
});
describe('/blocks/:height', function() {
it('fails with invalid height', function(cb) {
agent.get('/v1/blocks/-15')
.expect(422)
.expect('/v1/blocks/ parameter must be a 64 digit hex or block height integer', cb);
});
it('returns 404 with non existent block', function(cb) {
agent.get('/v1/blocks/876543')
.expect(404)
.expect('Block with height 876543 not found', cb);
});
it('works with valid height', function(cb) {
agent.get('/v1/blocks/100000')
.expect(200)
.expect(b1.toJSON(), cb);
});
});
});

39
api/test/v1/node.js Normal file
View File

@ -0,0 +1,39 @@
'use strict';
var chai = require('chai');
var should = chai.should();
var request = require('supertest');
var EventEmitter = require('eventemitter2').EventEmitter2;
var Promise = require('bluebird');
Promise.longStackTraces();
var BitcoreHTTP = require('../../lib/http');
describe('BitcoreHTTP v1 node routes', function() {
// mocks
var nodeMock, app, agent;
beforeEach(function() {
nodeMock = new EventEmitter();
nodeMock.status = {
sync: 0.75,
peer_count: 8,
version: 'test'
};
nodeMock.getStatus = function() {
return Promise.resolve(nodeMock.status);
};
app = new BitcoreHTTP(nodeMock).app;
agent = request(app);
});
describe('/node', function() {
it('works', function(cb) {
agent.get('/v1/node/')
.expect(200)
.expect(nodeMock.status, cb);
});
});
});

112
api/test/v1/transactions.js Normal file
View File

@ -0,0 +1,112 @@
'use strict';
var chai = require('chai');
var should = chai.should();
var request = require('supertest');
var bitcore = require('bitcore');
var _ = bitcore.deps._;
var Transaction = bitcore.Transaction;
var EventEmitter = require('eventemitter2').EventEmitter2;
var Promise = require('bluebird');
Promise.longStackTraces();
var BitcoreHTTP = require('../../lib/http');
var BitcoreNode = require('../../../');
var mockTransactions = require('../data/transactions');
describe('BitcoreHTTP v1 transactions routes', function() {
// mocks
var mockValidTx = new Transaction();
var t1 = mockTransactions[Object.keys(mockTransactions)[0]];
var nodeMock, app, agent;
beforeEach(function() {
nodeMock = new EventEmitter();
nodeMock.getTransaction = function(txHash) {
var tx = mockTransactions[txHash];
if (_.isUndefined(tx)) {
return Promise.reject(new BitcoreNode.errors.Transactions.NotFound(txHash));
}
return Promise.resolve(tx);
};
nodeMock.broadcast = function(tx) {
if (mockTransactions[tx.id]) {
return Promise.reject(new BitcoreNode.errors.Transactions.CantBroadcast(tx.id));
}
return Promise.resolve();
};
app = new BitcoreHTTP(nodeMock).app;
agent = request(app);
});
describe('/transactions', function() {
it('works with default parameters', function(cb) {
agent.get('/v1/transactions/')
.expect(200)
.expect({
'message': 'This is a mocked response'
}, cb);
});
});
describe('/transactions/:txHash', function() {
it('fails with invalid txHash', function(cb) {
agent.get('/v1/transactions/abad1dea')
.expect(422)
.expect('/v1/transactions/ parameter must be a 64 digit hex', cb);
});
it('returns 404 with non existent transaction', function(cb) {
agent.get('/v1/transactions/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b600000000')
.expect(404)
.expect('Transaction with id 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b600000000 not found', cb);
});
Object.keys(mockTransactions).forEach(function(hash) {
it('works with valid txHash ...' + hash.substring(hash.length - 8), function(cb) {
agent.get('/v1/transactions/' + hash)
.expect(200)
.expect(mockTransactions[hash].toJSON(), cb);
});
});
});
describe('/transactions/send', function() {
it('fails with invalid data type', function(cb) {
agent.post('/v1/transactions/send')
.send('some random data')
.expect(422)
.expect('/v1/transactions/send parameter must be a raw transaction hex', cb);
});
it('fails with invalid data format', function(cb) {
agent.post('/v1/transactions/send')
.send({
1: 2
})
.expect(422)
.expect('/v1/transactions/send parameter must be a raw transaction hex', cb);
});
it('fails with valid data format, invalid raw tx', function(cb) {
agent.post('/v1/transactions/send')
.send({
raw: '00abad1d3a'
})
.expect(422)
.expect('/v1/transactions/send parameter must be a raw transaction hex', cb);
});
it('works with valid tx', function(cb) {
agent.post('/v1/transactions/send')
.send({
raw: mockValidTx.uncheckedSerialize()
})
.expect(200)
.expect('Transaction broadcasted successfully', cb);
});
it('fails with invalid tx', function(cb) {
agent.post('/v1/transactions/send')
.send({
raw: t1.uncheckedSerialize()
})
.expect(422)
.expect('Unable to broadcast transaction 8c14f0db3df150123e6f3dbbf30f8b955a8249b62ac1d1ff16284aefa3d06d87', cb);
});
});
});

View File

@ -1,10 +1,10 @@
'use strict';
var config = require('config');
var BitcoreNode = require('./lib/node.js');
var BitcoreNode = require('./lib/node');
if (require.main === module) {
var config = require('config');
var node = BitcoreNode.create(config.get('BitcoreNode'));
node.start();
node.on('error', function(err) {
@ -16,4 +16,7 @@ if (require.main === module) {
});
}
BitcoreNode.errors = require('./lib/errors');
module.exports = BitcoreNode;

26
lib/errors.js Normal file
View File

@ -0,0 +1,26 @@
'use strict';
var spec = {
name: 'BitcoreNode',
message: 'Internal Error on BitcoreNode',
errors: [{
name: 'Transactions',
message: 'Internal Transactions error on BitcoreNode',
errors: [{
name: 'NotFound',
message: 'Transaction {0} not found'
}, {
name: 'CantBroadcast',
message: 'Unable to broadcast transaction {0}'
}]
}, {
name: 'Blocks',
message: 'Internal Blocks error on BitcoreNode',
errors: [{
name: 'NotFound',
message: 'Block {0} not found'
}]
}]
};
module.exports = require('bitcore').errors.extend(spec);