Merge pull request #43 from matiu/feat/api-tests

Feat/api tests
This commit is contained in:
Ivan Socolsky 2015-02-19 18:45:59 -03:00
commit 83ae3a7069
9 changed files with 752 additions and 367 deletions

250
app.js
View File

@ -1,252 +1,10 @@
'use strict';
var _ = require('lodash');
var async = require('async');
var log = require('npmlog');
var express = require('express');
var querystring = require('querystring');
var bodyParser = require('body-parser')
var CopayServer = require('./lib/server');
log.debug = log.verbose;
log.level = 'debug';
CopayServer.initialize();
var app = express();
app.use(function(req, res, next) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,Content-Type,Authorization');
next();
});
var allowCORS = function(req, res, next) {
if ('OPTIONS' == req.method) {
res.send(200);
res.end();
return;
}
next();
}
app.use(allowCORS);
var POST_LIMIT = 1024 * 100 /* Max POST 100 kb */ ;
app.use(bodyParser.json({
limit: POST_LIMIT
}));
app.use(require('morgan')('dev'));
#!/usr/bin/env node
var ExpressApp = require('./lib/expressapp');
var port = process.env.COPAY_PORT || 3001;
var router = express.Router();
function returnError(err, res, req) {
if (err instanceof CopayServer.ClientError) {
var status = (err.code == 'NOTAUTHORIZED') ? 401 : 400;
log.error('Err: ' + status + ':' + req.url + ' :' + err.code + ':' + err.message);
res.status(status).json({
code: err.code,
error: err.message,
}).end();
} else {
var code, message;
if (_.isObject(err)) {
code = err.code;
message = err.message;
}
var m = message || err.toString();
log.error('Error: ' + req.url + ' :' + code + ':' + m);
res.status(code || 500).json({
error: m,
}).end();
}
};
function getCredentials(req) {
var identity = req.header('x-identity');
if (!identity) return;
return {
copayerId: identity,
signature: req.header('x-signature'),
};
};
function getServerWithAuth(req, res, cb) {
var credentials = getCredentials(req);
var auth = {
copayerId: credentials.copayerId,
message: req.method.toLowerCase() + '|' + req.url + '|' + JSON.stringify(req.body),
signature: credentials.signature,
};
CopayServer.getInstanceWithAuth(auth, function(err, server) {
if (err) return returnError(err, res, req);
return cb(server);
});
};
router.post('/v1/wallets/', function(req, res) {
var server = CopayServer.getInstance();
server.createWallet(req.body, function(err, walletId) {
if (err) return returnError(err, res, req);
res.json({
walletId: walletId,
});
});
});
router.post('/v1/wallets/:id/copayers/', function(req, res) {
req.body.walletId = req.params['id'];
var server = CopayServer.getInstance();
server.joinWallet(req.body, function(err, result) {
if (err) return returnError(err, res, req);
res.json(result);
});
});
router.get('/v1/wallets/', function(req, res) {
getServerWithAuth(req, res, function(server) {
var result = {};
async.parallel([
function(next) {
server.getWallet({}, function(err, wallet) {
if (err) return next(err);
result.wallet = wallet;
next();
});
},
function(next) {
server.getBalance({}, function(err, balance) {
if (err) return next(err);
result.balance = balance;
next();
});
},
function(next) {
server.getPendingTxs({}, function(err, pendingTxps) {
if (err) return next(err);
result.pendingTxps = pendingTxps;
next();
});
},
], function(err) {
if (err) return returnError(err, res, req);
res.json(result);
});
});
});
router.get('/v1/txproposals/', function(req, res) {
getServerWithAuth(req, res, function(server) {
server.getPendingTxs({}, function(err, pendings) {
if (err) return returnError(err, res, req);
res.json(pendings);
});
});
});
router.post('/v1/txproposals/', function(req, res) {
getServerWithAuth(req, res, function(server) {
server.createTx(req.body, function(err, txp) {
if (err) return returnError(err, res, req);
res.json(txp);
});
});
});
router.post('/v1/addresses/', function(req, res) {
getServerWithAuth(req, res, function(server) {
server.createAddress(req.body, function(err, address) {
if (err) return returnError(err, res, req);
res.json(address);
});
});
});
router.get('/v1/addresses/', function(req, res) {
getServerWithAuth(req, res, function(server) {
server.getAddresses({}, function(err, addresses) {
if (err) return returnError(err, res, req);
res.json(addresses);
});
});
});
router.get('/v1/balance/', function(req, res) {
getServerWithAuth(req, res, function(server) {
server.getBalance({}, function(err, balance) {
if (err) return returnError(err, res, req);
res.json(balance);
});
});
});
router.post('/v1/txproposals/:id/signatures/', function(req, res) {
getServerWithAuth(req, res, function(server) {
req.body.txProposalId = req.params['id'];
server.signTx(req.body, function(err, txp) {
if (err) return returnError(err, res, req);
res.json(txp);
res.end();
});
});
});
// TODO Check HTTP verb and URL name
router.post('/v1/txproposals/:id/broadcast/', function(req, res) {
getServerWithAuth(req, res, function(server) {
req.body.txProposalId = req.params['id'];
server.broadcastTx(req.body, function(err, txp) {
if (err) return returnError(err, res, req);
res.json(txp);
res.end();
});
});
});
router.post('/v1/txproposals/:id/rejections', function(req, res) {
getServerWithAuth(req, res, function(server) {
req.body.txProposalId = req.params['id'];
server.rejectTx(req.body, function(err, txp) {
if (err) return returnError(err, res, req);
res.json(txp);
res.end();
});
});
});
router.delete('/v1/txproposals/:id/', function(req, res) {
getServerWithAuth(req, res, function(server) {
req.body.txProposalId = req.params['id'];
server.removePendingTx(req.body, function(err) {
if (err) return returnError(err, res, req);
res.end();
});
});
});
// TODO: DEBUG only!
router.get('/v1/dump', function(req, res) {
var server = CopayServer.getInstance();
server.storage._dump(function() {
res.end();
});
});
app.use('/copay/api', router);
var app = ExpressApp.start();
app.listen(port);
console.log('Copay service running on port ' + port);

View File

@ -35,7 +35,8 @@ Verifier.checkCopayers = function(copayers, walletPrivKey, myXPrivKey, n) {
}
// Not signed pub keys
if (!WalletUtils.verifyMessage(copayer.xPubKey, copayer.xPubKeySignature, walletPubKey)) {
if (!copayer.xPubKey || !copayer.xPubKeySignature ||
!WalletUtils.verifyMessage(copayer.xPubKey, copayer.xPubKeySignature, walletPubKey)) {
log.error('Invalid signatures in server response');
error = true;
}
@ -53,11 +54,12 @@ Verifier.checkCopayers = function(copayers, walletPrivKey, myXPrivKey, n) {
Verifier.checkTxProposal = function(data, txp) {
$.checkArgument(txp.creatorId);
var creatorXPubKey = _.find(data.publicKeyRing, function(xPubKey) {
if (WalletUtils.xPubToCopayerId(xPubKey) === txp.creatorId) return true;
});
if (!creatorXPubKey) return false;
var creatorSigningPubKey = (new Bitcore.HDPublicKey(creatorXPubKey)).derive('m/1/0').publicKey.toString();
var hash = WalletUtils.getProposalHash(txp.toAddress, txp.amount, txp.message);

View File

@ -60,8 +60,11 @@ function API(opts) {
this.verbose = !!opts.verbose;
this.request = request || opts.request;
this.baseUrl = opts.baseUrl || BASE_URL;
this.basePath = this.baseUrl.replace(/http.?:\/\/[a-zA-Z0-9:-]*\//,'/');
if (this.verbose) {
log.level = 'debug';
} else {
log.level = 'info';
}
};
@ -127,6 +130,8 @@ API.prototype._doRequest = function(method, url, args, data, cb) {
var absUrl = this.baseUrl + url;
var args = {
// relUrl: only for testing with `supertest`
relUrl: this.basePath + url,
headers: {
'x-identity': data.copayerId,
'x-signature': reqSignature,
@ -415,7 +420,7 @@ API.prototype.import = function(str, cb) {
var xPubKey = (new Bitcore.HDPublicKey(data.xPrivKey)).toString();
data.publicKeyRing.push(xPubKey);
data.publicKeyRing.unshift(xPubKey);
data.copayerId = WalletUtils.xPubToCopayerId(xPubKey);
data.n = data.publicKeyRing.length;
data.signingPrivKey = (new Bitcore.HDPrivateKey(data.xPrivKey)).derive('m/1/0').privateKey.toWIF();
@ -444,6 +449,7 @@ API.prototype.getTxProposals = function(opts, cb) {
API.prototype.signTxProposal = function(txp, cb) {
var self = this;
$.checkArgument(txp.creatorId);
this._loadAndCheck(function(err, data) {
if (err) return cb(err);
@ -452,7 +458,6 @@ API.prototype.signTxProposal = function(txp, cb) {
return cb(new ServerCompromisedError('Server sent fake transaction proposal'));
}
//Derive proper key to sign, for each input
var privs = [],
derived = {};
@ -493,6 +498,7 @@ API.prototype.signTxProposal = function(txp, cb) {
API.prototype.rejectTxProposal = function(txp, reason, cb) {
var self = this;
$.checkArgument(cb);
this._loadAndCheck(
function(err, data) {

259
lib/expressapp.js Normal file
View File

@ -0,0 +1,259 @@
'use strict';
var _ = require('lodash');
var async = require('async');
var log = require('npmlog');
var express = require('express');
var querystring = require('querystring');
var bodyParser = require('body-parser')
var CopayServer = require('./server');
log.debug = log.verbose;
log.level = 'debug';
var ExpressApp = function() {};
ExpressApp.start = function(opts) {
opts = opts || {};
CopayServer.initialize(opts.CopayServer);
var app = express();
app.use(function(req, res, next) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,Content-Type,Authorization');
next();
});
var allowCORS = function(req, res, next) {
if ('OPTIONS' == req.method) {
res.send(200);
res.end();
return;
}
next();
}
app.use(allowCORS);
var POST_LIMIT = 1024 * 100 /* Max POST 100 kb */ ;
app.use(bodyParser.json({
limit: POST_LIMIT
}));
app.use(require('morgan')('dev'));
var router = express.Router();
function returnError(err, res, req) {
if (err instanceof CopayServer.ClientError) {
var status = (err.code == 'NOTAUTHORIZED') ? 401 : 400;
log.error('Err: ' + status + ':' + req.url + ' :' + err.code + ':' + err.message);
res.status(status).json({
code: err.code,
error: err.message,
}).end();
} else {
var code, message;
if (_.isObject(err)) {
code = err.code;
message = err.message;
}
var m = message || err.toString();
log.error('Error: ' + req.url + ' :' + code + ':' + m);
res.status(code || 500).json({
error: m,
}).end();
}
};
function getCredentials(req) {
var identity = req.header('x-identity');
if (!identity) return;
return {
copayerId: identity,
signature: req.header('x-signature'),
};
};
function getServerWithAuth(req, res, cb) {
var credentials = getCredentials(req);
if (!credentials)
return returnError(new CopayServer.ClientError({
code: 'NOTAUTHORIZED'
}), res, req);
var auth = {
copayerId: credentials.copayerId,
message: req.method.toLowerCase() + '|' + req.url + '|' + JSON.stringify(req.body),
signature: credentials.signature,
};
CopayServer.getInstanceWithAuth(auth, function(err, server) {
if (err) return returnError(err, res, req);
return cb(server);
});
};
router.post('/v1/wallets/', function(req, res) {
var server = CopayServer.getInstance();
server.createWallet(req.body, function(err, walletId) {
if (err) return returnError(err, res, req);
res.json({
walletId: walletId,
});
});
});
router.post('/v1/wallets/:id/copayers/', function(req, res) {
req.body.walletId = req.params['id'];
var server = CopayServer.getInstance();
server.joinWallet(req.body, function(err, result) {
if (err) return returnError(err, res, req);
res.json(result);
});
});
router.get('/v1/wallets/', function(req, res) {
getServerWithAuth(req, res, function(server) {
var result = {};
async.parallel([
function(next) {
server.getWallet({}, function(err, wallet) {
if (err) return next(err);
result.wallet = wallet;
next();
});
},
function(next) {
server.getBalance({}, function(err, balance) {
if (err) return next(err);
result.balance = balance;
next();
});
},
function(next) {
server.getPendingTxs({}, function(err, pendingTxps) {
if (err) return next(err);
result.pendingTxps = pendingTxps;
next();
});
},
], function(err) {
if (err) return returnError(err, res, req);
res.json(result);
});
});
});
router.get('/v1/txproposals/', function(req, res) {
getServerWithAuth(req, res, function(server) {
server.getPendingTxs({}, function(err, pendings) {
if (err) return returnError(err, res, req);
res.json(pendings);
});
});
});
router.post('/v1/txproposals/', function(req, res) {
getServerWithAuth(req, res, function(server) {
server.createTx(req.body, function(err, txp) {
if (err) return returnError(err, res, req);
res.json(txp);
});
});
});
router.post('/v1/addresses/', function(req, res) {
getServerWithAuth(req, res, function(server) {
server.createAddress(req.body, function(err, address) {
if (err) return returnError(err, res, req);
res.json(address);
});
});
});
router.get('/v1/addresses/', function(req, res) {
getServerWithAuth(req, res, function(server) {
server.getAddresses({}, function(err, addresses) {
if (err) return returnError(err, res, req);
res.json(addresses);
});
});
});
router.get('/v1/balance/', function(req, res) {
getServerWithAuth(req, res, function(server) {
server.getBalance({}, function(err, balance) {
if (err) return returnError(err, res, req);
res.json(balance);
});
});
});
router.post('/v1/txproposals/:id/signatures/', function(req, res) {
getServerWithAuth(req, res, function(server) {
req.body.txProposalId = req.params['id'];
server.signTx(req.body, function(err, txp) {
if (err) return returnError(err, res, req);
res.json(txp);
res.end();
});
});
});
// TODO Check HTTP verb and URL name
router.post('/v1/txproposals/:id/broadcast/', function(req, res) {
getServerWithAuth(req, res, function(server) {
req.body.txProposalId = req.params['id'];
server.broadcastTx(req.body, function(err, txp) {
if (err) return returnError(err, res, req);
res.json(txp);
res.end();
});
});
});
router.post('/v1/txproposals/:id/rejections', function(req, res) {
getServerWithAuth(req, res, function(server) {
req.body.txProposalId = req.params['id'];
server.rejectTx(req.body, function(err, txp) {
if (err) return returnError(err, res, req);
res.json(txp);
res.end();
});
});
});
router.delete('/v1/txproposals/:id/', function(req, res) {
getServerWithAuth(req, res, function(server) {
req.body.txProposalId = req.params['id'];
server.removePendingTx(req.body, function(err) {
if (err) return returnError(err, res, req);
res.end();
});
});
});
// TODO: DEBUG only!
router.get('/v1/dump', function(req, res) {
var server = CopayServer.getInstance();
server.storage._dump(function() {
res.end();
});
});
app.use(opts.base_path || '/copay/api', router);
return app;
};
module.exports = ExpressApp;

View File

@ -26,15 +26,18 @@ var TxProposal = require('./model/txproposal');
var Notification = require('./model/notification');
var initialized = false;
var storage;
var storage, blockExplorer;
/**
* Creates an instance of the Copay server.
* @constructor
*/
function CopayServer() {
if (!initialized) throw new Error('Server not initialized');
if (!initialized)
throw new Error('Server not initialized');
this.storage = storage;
this.blockExplorer = blockExplorer;
this.notifyTicker = 0;
};
@ -45,10 +48,12 @@ nodeutil.inherits(CopayServer, events.EventEmitter);
* Initializes global settings for all instances.
* @param {Object} opts
* @param {Storage} [opts.storage] - The storage provider.
* @param {Storage} [opts.blockExplorer] - The blockExporer provider.
*/
CopayServer.initialize = function(opts) {
opts = opts || {};
storage = opts.storage ||  new Storage();
blockExplorer = opts.blockExplorer;
initialized = true;
};
@ -311,6 +316,9 @@ CopayServer.prototype.verifyMessageSignature = function(opts, cb) {
CopayServer.prototype._getBlockExplorer = function(provider, network) {
var url;
if (this.blockExplorer)
return this.blockExplorer;
switch (provider) {
default:
case 'insight':
@ -352,7 +360,6 @@ CopayServer.prototype._getUtxos = function(cb) {
var utxos = _.map(inutxos, function(i) {
return i.toObject();
});
self.getPendingTxs({}, function(err, txps) {
if (err) return cb(err);
@ -773,7 +780,7 @@ CopayServer.prototype.rejectTx = function(opts, cb) {
});
};
return cb();
return cb(null, txp);
});
});
};

View File

@ -1,4 +1,5 @@
var _ = require('lodash');
var $ = require('preconditions').singleton();
var sjcl = require('sjcl');
var Bitcore = require('bitcore');
@ -14,6 +15,7 @@ function WalletUtils() {};
/* TODO: It would be nice to be compatible with bitcoind signmessage. How
* the hash is calculated there? */
WalletUtils.hashMessage = function(text) {
$.checkArgument(text);
var buf = new Buffer(text);
var ret = crypto.Hash.sha256sha256(buf);
ret = new Bitcore.encoding.BufferReader(ret).readReverse();
@ -22,6 +24,7 @@ WalletUtils.hashMessage = function(text) {
WalletUtils.signMessage = function(text, privKey) {
$.checkArgument(text);
var priv = new PrivateKey(privKey);
var hash = WalletUtils.hashMessage(text);
return crypto.ECDSA.sign(hash, priv, 'little').toString();
@ -29,6 +32,11 @@ WalletUtils.signMessage = function(text, privKey) {
WalletUtils.verifyMessage = function(text, signature, pubKey) {
$.checkArgument(text, pubKey);
if (!signature)
return false;
var pub = new PublicKey(pubKey);
var hash = WalletUtils.hashMessage(text);
@ -69,6 +77,7 @@ WalletUtils.toSecret = function(walletId, walletPrivKey, network) {
};
WalletUtils.fromSecret = function(secret) {
$.checkArgument(secret);
var secretSplit = secret.split(':');
var walletId = secretSplit[0];
var walletPrivKey = Bitcore.PrivateKey.fromString(secretSplit[1]);
@ -104,8 +113,10 @@ WalletUtils.UNITS = {
};
WalletUtils.parseAmount = function(text) {
var regex = '^(\\d*(\\.\\d{0,8})?)\\s*(' + _.keys(WalletUtils.UNITS).join('|') + ')?$';
if (!_.isString(text))
text = text.toString();
var regex = '^(\\d*(\\.\\d{0,8})?)\\s*(' + _.keys(WalletUtils.UNITS).join('|') + ')?$';
var match = new RegExp(regex, 'i').exec(text.trim());
if (!match || match.length === 0) throw new Error('Invalid amount');

View File

@ -40,7 +40,8 @@
"mocha": "^1.18.2",
"sinon": "^1.10.3",
"memdown": "^1.0.0",
"jsdoc": "^3.3.0"
"jsdoc": "^3.3.0",
"supertest": "*"
},
"scripts": {
"start": "node server.js"

View File

@ -4,133 +4,497 @@ var _ = require('lodash');
var chai = require('chai');
var sinon = require('sinon');
var should = chai.should();
var levelup = require('levelup');
var memdown = require('memdown');
var async = require('async');
var request = require('supertest');
var Client = require('../../lib/client');
var API = Client.API;
var Bitcore = require('bitcore');
var TestData = require('./clienttestdata');
var WalletUtils = require('../../lib/walletutils');
var ExpressApp = require('../../lib/expressapp');
var Storage = require('../../lib/storage');
var helpers = {};
helpers.getRequest = function(app) {
return function(args, cb) {
var req = request(app);
var r = req[args.method](args.relUrl);
if (args.headers) {
_.each(args.headers, function(v, k) {
r.set(k, v);
})
}
if (!_.isEmpty(args.body)) {
r.send(args.body);
};
r.end(function(err, res) {
return cb(err, res, res.body);
});
};
};
helpers.createAndJoinWallet = function(clients, m, n, cb) {
clients[0].createWallet('wallet name', 'creator', m, n, 'testnet',
function(err, secret) {
if (err) return cb(err);
if (n == 1) return cb();
should.exist(secret);
async.each(_.range(n - 1), function(i, cb) {
clients[i + 1].joinWallet(secret, 'copayer ' + (i + 1), function(err, result) {
should.not.exist(err);
return cb(err);
});
}, function(err) {
if (err) return cb(err);
return cb(null, {
m: m,
n: n,
secret: secret,
});
});
});
};
var fsmock = {};
var content = {};
fsmock.readFile = function(name, enc, cb) {
if (!content || _.isEmpty(content[name]))
return cb('empty');
return cb(null, content[name]);
};
fsmock.writeFile = function(name, data, cb) {
content[name] = data;
return cb();
};
fsmock.reset = function() {
content = {};
};
fsmock._get = function(name) {
return content[name];
};
var blockExplorerMock = {};
blockExplorerMock.utxos = [];
blockExplorerMock.getUnspentUtxos = function(dummy, cb) {
var ret = _.map(blockExplorerMock.utxos || [], function(x) {
x.toObject = function() {
return this;
};
return x;
});
return cb(null, ret);
};
blockExplorerMock.setUtxo = function(address, amount, m) {
blockExplorerMock.utxos.push({
txid: Bitcore.crypto.Hash.sha256(new Buffer(Math.random() * 100000)).toString('hex'),
vout: Math.floor((Math.random() * 10) + 1),
amount: amount,
address: address.address,
scriptPubKey: Bitcore.Script.buildMultisigOut(address.publicKeys, m).toScriptHashOut().toString(),
});
};
blockExplorerMock.broadcast = function(raw, cb) {
blockExplorerMock.lastBroadcasted = raw;
return cb(null, (new Bitcore.Transaction(raw)).id);
};
blockExplorerMock.reset = function() {
blockExplorerMock.utxos = [];
};
describe('client API ', function() {
var client;
var clients, app;
beforeEach(function() {
var fsmock = {};;
fsmock.readFile = sinon.mock().yields(null, JSON.stringify(TestData.storage.wallet11));
fsmock.writeFile = sinon.mock().yields();
var storage = new Client.FileStorage({
filename: 'dummy',
fs: fsmock,
clients = [];
var db = levelup(memdown, {
valueEncoding: 'json'
});
client = new Client({
storage: storage
var storage = new Storage({
db: db
});
app = ExpressApp.start({
CopayServer: {
storage: storage,
blockExplorer: blockExplorerMock,
}
});
// Generates 5 clients
_.each(_.range(5), function(i) {
var storage = new Client.FileStorage({
filename: 'client' + i,
fs: fsmock,
});
var client = new Client({
storage: storage,
});
client.request = helpers.getRequest(app);
clients.push(client);
});
fsmock.reset();
blockExplorerMock.reset();
});
describe('#_tryToComplete ', function() {
it('should complete a wallet ', function(done) {
var request = sinon.stub();
// Wallet request
request.onCall(0).yields(null, {
statusCode: 200,
}, TestData.serverResponse.completeWallet);
request.onCall(1).yields(null, {
statusCode: 200,
}, "pepe");
client.request = request;
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22));
client.getBalance(function(err, x) {
describe('Wallet Creation', function() {
it('should check balance in a 1-1 ', function(done) {
helpers.createAndJoinWallet(clients, 1, 1, function(err) {
should.not.exist(err);
done();
clients[0].getBalance(function(err, x) {
should.not.exist(err);
done();
})
});
});
it('should be able to complete wallets in copayer that joined later', function(done) {
helpers.createAndJoinWallet(clients, 2, 3, function(err) {
should.not.exist(err);
clients[0].getBalance(function(err, x) {
should.not.exist(err);
clients[1].getBalance(function(err, x) {
should.not.exist(err);
clients[2].getBalance(function(err, x) {
should.not.exist(err);
done();
})
})
})
});
});
it('should handle incomple wallets', function(done) {
var request = sinon.stub();
// Wallet request
request.onCall(0).yields(null, {
statusCode: 200,
}, TestData.serverResponse.incompleteWallet);
client.request = request;
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22));
client.createAddress(function(err, x) {
err.should.contain('Incomplete');
it('should not allow to join a full wallet ', function(done) {
helpers.createAndJoinWallet(clients, 2, 2, function(err, w) {
should.not.exist(err);
should.exist(w.secret);
clients[4].joinWallet(w.secret, 'copayer', function(err, result) {
err.should.contain('Request error');
done();
});
});
});
it('should fail with a unknown secret', function(done) {
var oldSecret = '3f8e5acb-ceeb-4aae-134f-692d934e3b1c:L2gohj8s2fLKqVU5cQutAVGciutUxczFxLxxXHFsjzLh71ZjkFQQ:T';
clients[0].joinWallet(oldSecret, 'copayer', function(err, result) {
err.should.contain('Request error');
done();
});
});
it('should reject wallets with bad signatures', function(done) {
var request = sinon.stub();
// Wallet request
request.onCall(0).yields(null, {
statusCode: 200,
}, TestData.serverResponse.corruptWallet22);
helpers.createAndJoinWallet(clients, 2, 3, function(err) {
should.not.exist(err);
client.request = request;
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22));
client.createAddress(function(err, x) {
err.should.contain('verified');
done();
// Get right response
var data = clients[0]._load(function(err, data) {
var url = '/v1/wallets/';
clients[0]._doGetRequest(url, data, function(err, x) {
// Tamper data
x.wallet.copayers[0].xPubKey = x.wallet.copayers[1].xPubKey;
// Tamper response
clients[1]._doGetRequest = sinon.stub().yields(null, x);
clients[1].getBalance(function(err, x) {
err.should.contain('verified');
done();
});
});
});
});
});
it('should reject wallets with missing signatures ', function(done) {
var request = sinon.stub();
// Wallet request
request.onCall(0).yields(null, {
statusCode: 200,
}, TestData.serverResponse.corruptWallet222);
it('should reject wallets with missing signatures', function(done) {
helpers.createAndJoinWallet(clients, 2, 3, function(err) {
should.not.exist(err);
client.request = request;
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22));
client.createAddress(function(err, x) {
err.should.contain('verified');
done();
// Get right response
var data = clients[0]._load(function(err, data) {
var url = '/v1/wallets/';
clients[0]._doGetRequest(url, data, function(err, x) {
// Tamper data
delete x.wallet.copayers[1].xPubKey;
// Tamper response
clients[1]._doGetRequest = sinon.stub().yields(null, x);
clients[1].getBalance(function(err, x) {
err.should.contain('verified');
done();
});
});
});
});
});
it('should reject wallets missing caller"s pubkey', function(done) {
var request = sinon.stub();
// Wallet request
request.onCall(0).yields(null, {
statusCode: 200,
}, TestData.serverResponse.missingMyPubKey);
helpers.createAndJoinWallet(clients, 2, 3, function(err) {
should.not.exist(err);
client.request = request;
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22));
client.createAddress(function(err, x) {
err.should.contain('verified');
done();
// Get right response
var data = clients[0]._load(function(err, data) {
var url = '/v1/wallets/';
clients[0]._doGetRequest(url, data, function(err, x) {
// Tamper data. Replace caller's pubkey
x.wallet.copayers[1].xPubKey = (new Bitcore.HDPrivateKey()).publicKey;
// Add a correct signature
x.wallet.copayers[1].xPubKeySignature = WalletUtils.signMessage(
x.wallet.copayers[1].xPubKey, data.walletPrivKey),
// Tamper response
clients[1]._doGetRequest = sinon.stub().yields(null, x);
clients[1].getBalance(function(err, x) {
err.should.contain('verified');
done();
});
});
});
});
});
});
describe('#createAddress ', function() {
it('should check address ', function(done) {
var response = {
createdOn: 1424105995,
address: '2N3fA6wDtnebzywPkGuNK9KkFaEzgbPRRTq',
path: 'm/2147483647/0/7',
publicKeys: ['03f6a5fe8db51bfbaf26ece22a3e3bc242891a47d3048fc70bc0e8c03a071ad76f']
};
var request = sinon.mock().yields(null, {
statusCode: 200
}, response);
client.request = request;
client.createAddress(function(err, x) {
describe('Address Creation', function() {
it('should be able to create address in all copayers in a 2-3 wallet', function(done) {
this.timeout(5000);
helpers.createAndJoinWallet(clients, 2, 3, function(err) {
should.not.exist(err);
x.address.should.equal('2N3fA6wDtnebzywPkGuNK9KkFaEzgbPRRTq');
done();
clients[0].createAddress(function(err, x0) {
should.not.exist(err);
should.exist(x0.address);
clients[1].createAddress(function(err, x1) {
should.not.exist(err);
should.exist(x1.address);
clients[2].createAddress(function(err, x2) {
should.not.exist(err);
should.exist(x2.address);
done();
});
});
});
});
});
it('should see balance on address created by others', function(done) {
helpers.createAndJoinWallet(clients, 2, 2, function(err, w) {
should.not.exist(err);
clients[0].createAddress(function(err, x0) {
should.not.exist(err);
should.exist(x0.address);
blockExplorerMock.setUtxo(x0, 10, w.m);
clients[0].getBalance(function(err, bal0) {
should.not.exist(err);
bal0.totalAmount.should.equal(10 * 1e8);
bal0.lockedAmount.should.equal(0);
clients[1].getBalance(function(err, bal1) {
bal1.totalAmount.should.equal(10 * 1e8);
bal1.lockedAmount.should.equal(0);
done();
});
});
});
});
});
});
describe('Wallet Backups and Mobility', function() {
it('round trip #import #export', function(done) {
helpers.createAndJoinWallet(clients, 2, 2, function(err, w) {
should.not.exist(err);
clients[0].export(function(err, str) {
should.not.exist(err);
var original = JSON.parse(fsmock._get('client0'));
clients[2].import(str, function(err, wallet) {
should.not.exist(err);
var clone = JSON.parse(fsmock._get('client2'));
delete original.walletPrivKey; // no need to persist it.
clone.should.deep.equal(original);
done();
});
});
});
});
it('should recreate a wallet, create addresses and receive money', function(done) {
var backup = '["tprv8ZgxMBicQKsPehCdj4HM1MZbKVXBFt5Dj9nQ44M99EdmdiUfGtQBDTSZsKmzdUrB1vEuP6ipuoa39UXwPS2CvnjE1erk5aUjc5vQZkWvH4B",2,["tpubD6NzVbkrYhZ4XCNDPDtyRWPxvJzvTkvUE2cMPB8jcUr9Dkicv6cYQmA18DBAid6eRK1BGCU9nzgxxVdQUGLYJ34XsPXPW4bxnH4PH6oQBF3"],"sd0kzXmlXBgTGHrKaBW4aA=="]';
clients[0].import(backup, function(err, wallet) {
should.not.exist(err);
clients[0].reCreateWallet('pepe', function(err, wallet) {
should.not.exist(err);
clients[0].createAddress(function(err, x0) {
should.not.exist(err);
should.exist(x0.address);
blockExplorerMock.setUtxo(x0, 10, 2);
clients[0].getBalance(function(err, bal0) {
should.not.exist(err);
bal0.totalAmount.should.equal(10 * 1e8);
bal0.lockedAmount.should.equal(0);
done();
});
});
});
});
});
});
describe('Send Transactions', function() {
it('Send and broadcast in 1-1 wallet', function(done) {
helpers.createAndJoinWallet(clients, 1, 1, function(err, w) {
clients[0].createAddress(function(err, x0) {
should.not.exist(err);
should.exist(x0.address);
blockExplorerMock.setUtxo(x0, 1, 1);
var opts = {
amount: '0.1btc',
toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5',
message: 'hola 1-1',
};
clients[0].sendTxProposal(opts, function(err, x) {
should.not.exist(err);
x.requiredRejections.should.equal(1);
x.requiredSignatures.should.equal(1);
x.status.should.equal('pending');
x.changeAddress.path.should.equal('m/2147483647/1/0');
clients[0].signTxProposal(x, function(err, tx) {
should.not.exist(err);
tx.status.should.equal('broadcasted');
tx.txid.should.equal((new Bitcore.Transaction(blockExplorerMock.lastBroadcasted)).id);
done();
});
});
});
});
});
it('Send and broadcast in 2-3 wallet', function(done) {
helpers.createAndJoinWallet(clients, 2, 3, function(err, w) {
clients[0].createAddress(function(err, x0) {
should.not.exist(err);
should.exist(x0.address);
blockExplorerMock.setUtxo(x0, 10, 1);
var opts = {
amount: 10000,
toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5',
message: 'hola 1-1',
};
clients[0].sendTxProposal(opts, function(err, x) {
should.not.exist(err);
x.status.should.equal('pending');
x.requiredRejections.should.equal(2);
x.requiredSignatures.should.equal(2);
clients[0].signTxProposal(x, function(err, tx) {
should.not.exist(err, err);
tx.status.should.equal('pending');
clients[1].signTxProposal(x, function(err, tx) {
should.not.exist(err);
tx.status.should.equal('broadcasted');
tx.txid.should.equal((new Bitcore.Transaction(blockExplorerMock.lastBroadcasted)).id);
done();
});
});
});
});
});
});
it('Send, reject, 2 signs and broadcast in 2-3 wallet', function(done) {
helpers.createAndJoinWallet(clients, 2, 3, function(err, w) {
clients[0].createAddress(function(err, x0) {
should.not.exist(err);
should.exist(x0.address);
blockExplorerMock.setUtxo(x0, 10, 1);
var opts = {
amount: 10000,
toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5',
message: 'hola 1-1',
};
clients[0].sendTxProposal(opts, function(err, x) {
should.not.exist(err);
x.status.should.equal('pending');
x.requiredRejections.should.equal(2);
x.requiredSignatures.should.equal(2);
clients[0].rejectTxProposal(x, 'no me gusto', function(err, tx) {
should.not.exist(err, err);
tx.status.should.equal('pending');
clients[1].signTxProposal(x, function(err, tx) {
should.not.exist(err);
clients[2].signTxProposal(x, function(err, tx) {
should.not.exist(err);
tx.status.should.equal('broadcasted');
tx.txid.should.equal((new Bitcore.Transaction(blockExplorerMock.lastBroadcasted)).id);
done();
});
});
});
});
});
});
});
it('Send, reject in 3-4 wallet', function(done) {
helpers.createAndJoinWallet(clients, 3, 4, function(err, w) {
clients[0].createAddress(function(err, x0) {
should.not.exist(err);
should.exist(x0.address);
blockExplorerMock.setUtxo(x0, 10, 1);
var opts = {
amount: 10000,
toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5',
message: 'hola 1-1',
};
clients[0].sendTxProposal(opts, function(err, x) {
should.not.exist(err);
x.status.should.equal('pending');
x.requiredRejections.should.equal(2);
x.requiredSignatures.should.equal(3);
clients[0].rejectTxProposal(x, 'no me gusto', function(err, tx) {
should.not.exist(err, err);
tx.status.should.equal('pending');
clients[1].signTxProposal(x, function(err, tx) {
should.not.exist(err);
tx.status.should.equal('pending');
clients[2].rejectTxProposal(x, 'tampoco me gusto', function(err, tx) {
should.not.exist(err);
tx.status.should.equal('rejected');
done();
});
});
});
});
});
});
});
});
/*
describe('TODO', function(x) {
it('should detect fake addresses ', function(done) {
var response = {
createdOn: 1424105995,
@ -151,25 +515,6 @@ describe('client API ', function() {
});
describe('#export & #import 2-2 wallet', function() {
it('round trip ', function(done) {
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.complete22));
client.export(function(err, str) {
should.not.exist(err);
client.storage.fs.readFile = sinon.stub().yields(null);
client.import(str, function(err, wallet) {
should.not.exist(err);
var wallet = JSON.parse(client.storage.fs.writeFile.getCall(0).args[1]);
TestData.storage.complete22.should.deep.equal(wallet);
done();
});
});
});
});
describe('#getTxProposals', function() {
it('should return tx proposals and decrypt message', function(done) {
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.complete11));
@ -188,10 +533,6 @@ describe('client API ', function() {
});
});
describe('#recreate', function() {
it.skip('Should recreate a wallet acording stored data', function(done) {});
});
describe('#sendTxProposal ', function() {
it('should send tx proposal with encrypted message', function(done) {
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.complete11));
@ -261,4 +602,5 @@ describe('client API ', function() {
});
});
});
*/
});

View File

@ -20,7 +20,6 @@ var Copayer = require('../../lib/model/copayer');
var CopayServer = require('../../lib/server');
var TestData = require('../testdata');
var helpers = {};
helpers.getAuthServer = function(copayerId, cb) {
var signatureStub = sinon.stub(CopayServer.prototype, '_verifySignature');