add api tests

This commit is contained in:
Matias Alejo Garcia 2015-02-19 17:38:48 -03:00
parent 83f59ddfbb
commit ce8aeee3a9
5 changed files with 254 additions and 112 deletions

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;
}

View File

@ -420,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();

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);

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,7 @@ WalletUtils.signMessage = function(text, privKey) {
WalletUtils.verifyMessage = function(text, signature, pubKey) {
$.checkArgument(signature, text, pubKey);
var pub = new PublicKey(pubKey);
var hash = WalletUtils.hashMessage(text);
@ -69,6 +73,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]);
@ -105,7 +110,6 @@ WalletUtils.UNITS = {
WalletUtils.parseAmount = function(text) {
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

@ -39,7 +39,7 @@ helpers.getRequest = function(app) {
};
helpers.createAndJoinWallet = function(clients, m, n, cb) {
clients[0].createWallet('wallet name', 'creator copayer', m, n, 'testnet',
clients[0].createWallet('wallet name', 'creator', m, n, 'testnet',
function(err, secret) {
if (err) return cb(err);
if (n == 1) return cb();
@ -51,12 +51,17 @@ helpers.createAndJoinWallet = function(clients, m, n, cb) {
return cb(err);
});
}, function(err) {
if (err) return new Error('Could not generate wallet');
return cb();
if (err) return cb(err);
return cb(null, {
m: m,
n: n,
secret: secret,
});
});
});
};
var fsmock = {};
var content = {};
fsmock.readFile = function(name, enc, cb) {
@ -69,9 +74,47 @@ fsmock.writeFile = function(name, data, cb) {
content[name] = data;
return cb();
};
fsmock.reset = function() {
content = {};
};
fsmock._get = function(name) {
return content[name];
};
var utxos = [];
var blockExplorerMock = {};
blockExplorerMock.getUnspentUtxos = function(dummy, cb) {
var ret = _.map(utxos || [], function(x) {
x.toObject = function() {
return this;
};
return x;
});
return cb(null, ret);
};
blockExplorerMock.setUtxo = function(address, amount, m) {
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(),
});
};
blockExplorerMock.reset = function() {
utxos = [];
};
describe('client API ', function() {
var clients;
var clients, app;
beforeEach(function() {
clients = [];
@ -81,9 +124,10 @@ describe('client API ', function() {
var storage = new Storage({
db: db
});
var app = ExpressApp.start({
app = ExpressApp.start({
CopayServer: {
storage: storage
storage: storage,
blockExplorer: blockExplorerMock,
}
});
// Generates 5 clients
@ -99,10 +143,11 @@ describe('client API ', function() {
client.request = helpers.getRequest(app);
clients.push(client);
});
content = {};
fsmock.reset();
blockExplorerMock.reset();
});
describe.only('#getBalance', function() {
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);
@ -112,7 +157,7 @@ describe('client API ', function() {
})
});
});
it('should be able to check balance in a 2-3 wallet ', function(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) {
@ -127,105 +172,212 @@ describe('client API ', function() {
})
});
});
});
describe('#_tryToComplete ', function() {
it('should complete a wallet ', function(done) {
client.storage.fs.readFile =
sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22));
client.getBalance(function(err, x) {
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 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');
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.only('Send TXs', 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, 10, 1);
var opts = {
amount: 1000,
toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5',
message: 'hola 1-1',
};
clients[0].sendTxProposal(opts, function(err, x) {
should.not.exist(err);
console.log('[clientApi.js.372]',x); //TODO
done();
});
});
});
});
});
/*
describe('TODO', function(x) {
it('should detect fake addresses ', function(done) {
var response = {
createdOn: 1424105995,
@ -246,25 +398,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));
@ -283,10 +416,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));
@ -356,4 +485,5 @@ describe('client API ', function() {
});
});
});
*/
});