Browse Source

Merge pull request #80 from isocolsky/coverage

Increase coverage
activeAddress
Matias Alejo Garcia 10 years ago
parent
commit
35c48066b3
  1. 11
      lib/expressapp.js
  2. 4
      lib/model/txproposal.js
  3. 32
      lib/server.js
  4. 42
      lib/storage.js
  5. 88
      test/integration/clientApi.js
  6. 422
      test/integration/server.js
  7. 2
      test/models/addressmanager.js
  8. 2
      test/models/txproposal.js
  9. 2
      test/models/wallet.js
  10. 51
      test/utils.js

11
lib/expressapp.js

@ -64,7 +64,7 @@ ExpressApp.start = function(opts) {
var status = (err.code == 'NOTAUTHORIZED') ? 401 : 400; var status = (err.code == 'NOTAUTHORIZED') ? 401 : 400;
if (!opts.disableLogs) if (!opts.disableLogs)
log.info('Client Err: ' + status + ' ' + req.url + ' ' + err); log.info('Client Err: ' + status + ' ' + req.url + ' ' + err);
res.status(status).json({ res.status(status).json({
code: err.code, code: err.code,
@ -273,15 +273,6 @@ ExpressApp.start = function(opts) {
}); });
}); });
// TODO: DEBUG only!
router.get('/v1/dump', function(req, res) {
var server = WalletService.getInstance();
server.storage._dump(function() {
res.end();
});
});
app.use(opts.basePath || '/copay/api', router); app.use(opts.basePath || '/copay/api', router);
return app; return app;
}; };

4
lib/model/txproposal.js

@ -52,6 +52,7 @@ TxProposal.fromObj = function(obj) {
x.requiredRejections = obj.requiredRejections; x.requiredRejections = obj.requiredRejections;
x.status = obj.status; x.status = obj.status;
x.txid = obj.txid; x.txid = obj.txid;
x.broadcastedOn = obj.broadcastedOn;
x.inputPaths = obj.inputPaths; x.inputPaths = obj.inputPaths;
x.actions = _.map(obj.actions, function(action) { x.actions = _.map(obj.actions, function(action) {
return TxProposalAction.fromObj(action); return TxProposalAction.fromObj(action);
@ -172,7 +173,7 @@ TxProposal.prototype._addSignaturesToBitcoreTx = function(t, signatures, xpub) {
sigtype: Bitcore.crypto.Signature.SIGHASH_ALL, sigtype: Bitcore.crypto.Signature.SIGHASH_ALL,
publicKey: pub, publicKey: pub,
}; };
t.inputs[i].addSignature(t,s); t.inputs[i].addSignature(t, s);
i++; i++;
} catch (e) {}; } catch (e) {};
}); });
@ -220,6 +221,7 @@ TxProposal.prototype.isBroadcasted = function() {
TxProposal.prototype.setBroadcasted = function(txid) { TxProposal.prototype.setBroadcasted = function(txid) {
this.txid = txid; this.txid = txid;
this.status = 'broadcasted'; this.status = 'broadcasted';
this.broadcastedOn = Math.floor(Date.now() / 1000);
}; };
module.exports = TxProposal; module.exports = TxProposal;

32
lib/server.js

@ -119,8 +119,8 @@ WalletService.prototype.createWallet = function(opts, cb) {
try { try {
pubKey = new PublicKey.fromString(opts.pubKey); pubKey = new PublicKey.fromString(opts.pubKey);
} catch (e) { } catch (ex) {
return cb(e.toString()); return cb(new ClientError('Invalid public key'));
}; };
var wallet = Wallet.create({ var wallet = Wallet.create({
@ -512,6 +512,9 @@ WalletService.prototype.createTx = function(opts, cb) {
if (toAddress.network != wallet.getNetworkName()) if (toAddress.network != wallet.getNetworkName())
return cb(new ClientError('INVALIDADDRESS', 'Incorrect address network')); return cb(new ClientError('INVALIDADDRESS', 'Incorrect address network'));
if (opts.amount <= 0)
return cb(new ClientError('Invalid amount'));
if (opts.amount < Bitcore.Transaction.DUST_AMOUNT) if (opts.amount < Bitcore.Transaction.DUST_AMOUNT)
return cb(new ClientError('DUSTAMOUNT', 'Amount below dust threshold')); return cb(new ClientError('DUSTAMOUNT', 'Amount below dust threshold'));
@ -538,8 +541,7 @@ WalletService.prototype.createTx = function(opts, cb) {
try { try {
self._selectUtxos(txp, utxos); self._selectUtxos(txp, utxos);
} catch (ex) { } catch (ex) {
console.log('[server.js.523:ex:]', ex); //TODO return cb(new ClientError(ex.toString()));
return cb(new ClientError(ex));
} }
if (!txp.inputs) if (!txp.inputs)
@ -567,13 +569,13 @@ WalletService.prototype.createTx = function(opts, cb) {
/** /**
* Retrieves a tx from storage. * Retrieves a tx from storage.
* @param {Object} opts * @param {Object} opts
* @param {string} opts.id - The tx id. * @param {string} opts.txProposalId - The tx id.
* @returns {Object} txProposal * @returns {Object} txProposal
*/ */
WalletService.prototype.getTx = function(opts, cb) { WalletService.prototype.getTx = function(opts, cb) {
var self = this; var self = this;
self.storage.fetchTx(self.walletId, opts.id, function(err, txp) { self.storage.fetchTx(self.walletId, opts.txProposalId, function(err, txp) {
if (err) return cb(err); if (err) return cb(err);
if (!txp) return cb(new ClientError('Transaction proposal not found')); if (!txp) return cb(new ClientError('Transaction proposal not found'));
return cb(null, txp); return cb(null, txp);
@ -612,12 +614,12 @@ WalletService.prototype.removePendingTx = function(opts, cb) {
Utils.runLocked(self.walletId, cb, function(cb) { Utils.runLocked(self.walletId, cb, function(cb) {
self.getTx({ self.getTx({
id: opts.txProposalId, txProposalId: opts.txProposalId,
}, function(err, txp) { }, function(err, txp) {
if (err) return cb(err); if (err) return cb(err);
if (!txp.isPending()) if (!txp.isPending())
return cb(new ClientError('Transaction proposal not pending')); return cb(new ClientError('TXNOTPENDING', 'Transaction proposal not pending'));
if (txp.creatorId !== self.copayerId) if (txp.creatorId !== self.copayerId)
@ -626,7 +628,7 @@ WalletService.prototype.removePendingTx = function(opts, cb) {
var actors = txp.getActors(); var actors = txp.getActors();
if (actors.length > 1 || (actors.length == 1 && actors[0] !== self.copayerId)) if (actors.length > 1 || (actors.length == 1 && actors[0] !== self.copayerId))
return cb(new ClientError('Cannot remove a proposal signed/rejected by other copayers')); return cb(new ClientError('TXACTIONED', 'Cannot remove a proposal signed/rejected by other copayers'));
self._notify('transactionProposalRemoved'); self._notify('transactionProposalRemoved');
self.storage.removeTx(self.walletId, txp.id, cb); self.storage.removeTx(self.walletId, txp.id, cb);
@ -664,7 +666,7 @@ WalletService.prototype.signTx = function(opts, cb) {
if (err) return cb(err); if (err) return cb(err);
self.getTx({ self.getTx({
id: opts.txProposalId txProposalId: opts.txProposalId
}, function(err, txp) { }, function(err, txp) {
if (err) return cb(err); if (err) return cb(err);
@ -673,7 +675,7 @@ WalletService.prototype.signTx = function(opts, cb) {
}); });
if (action) if (action)
return cb(new ClientError('CVOTED', 'Copayer already voted on this transaction proposal')); return cb(new ClientError('CVOTED', 'Copayer already voted on this transaction proposal'));
if (txp.status != 'pending') if (!txp.isPending())
return cb(new ClientError('TXNOTPENDING', 'The transaction proposal is not pending')); return cb(new ClientError('TXNOTPENDING', 'The transaction proposal is not pending'));
var copayer = wallet.getCopayer(self.copayerId); var copayer = wallet.getCopayer(self.copayerId);
@ -689,9 +691,7 @@ WalletService.prototype.signTx = function(opts, cb) {
copayerId: self.copayerId, copayerId: self.copayerId,
}); });
// TODO: replace with .isAccepted() if (txp.isAccepted()) {
if (txp.status == 'accepted') {
self._notify('TxProposalFinallyAccepted', { self._notify('TxProposalFinallyAccepted', {
txProposalId: opts.txProposalId, txProposalId: opts.txProposalId,
}); });
@ -719,7 +719,7 @@ WalletService.prototype.broadcastTx = function(opts, cb) {
if (err) return cb(err); if (err) return cb(err);
self.getTx({ self.getTx({
id: opts.txProposalId txProposalId: opts.txProposalId
}, function(err, txp) { }, function(err, txp) {
if (err) return cb(err); if (err) return cb(err);
@ -761,7 +761,7 @@ WalletService.prototype.rejectTx = function(opts, cb) {
return cb(new ClientError('Required argument missing')); return cb(new ClientError('Required argument missing'));
self.getTx({ self.getTx({
id: opts.txProposalId txProposalId: opts.txProposalId
}, function(err, txp) { }, function(err, txp) {
if (err) return cb(err); if (err) return cb(err);

42
lib/storage.js

@ -22,7 +22,7 @@ var Storage = function(opts) {
}; };
var zeroPad = function(x, length) { var zeroPad = function(x, length) {
return (Array(length).join('0') + parseInt(x)).slice(-length); return _.padLeft(parseInt(x), length, '0');
}; };
var walletPrefix = function(id) { var walletPrefix = function(id) {
@ -33,10 +33,7 @@ var opKey = function(key) {
return key ? '!' + key : ''; return key ? '!' + key : '';
}; };
var MAX_TS = Array(14).join('9'); var MAX_TS = _.repeat('9', 14);
var opKeyTs = function(key) {
return key ? '!' + zeroPad(key, 14) : '';
};
var KEY = { var KEY = {
@ -106,16 +103,6 @@ Storage.prototype.fetchCopayerLookup = function(copayerId, cb) {
}); });
}; };
Storage.prototype.fetchNotification = function(walletId, notificationId, cb) {
this.db.get(KEY.NOTIFICATION(walletId, notificationId), function(err, data) {
if (err) {
if (err.notFound) return cb();
return cb(err);
}
return cb(null, Notification.fromObj(data));
});
};
Storage.prototype._completeTxData = function(walletId, txs, cb) { Storage.prototype._completeTxData = function(walletId, txs, cb) {
var txList = [].concat(txs); var txList = [].concat(txs);
this.fetchWallet(walletId, function(err, wallet) { this.fetchWallet(walletId, function(err, wallet) {
@ -293,8 +280,6 @@ Storage.prototype._delByKey = function(key, cb) {
}) })
.on('error', function(err) { .on('error', function(err) {
if (err.notFound) return cb(); if (err.notFound) return cb();
console.log('[storage.js.252]'); //TODO
return cb(err); return cb(err);
}) })
.on('end', function(err) { .on('end', function(err) {
@ -309,15 +294,6 @@ Storage.prototype._delByKey = function(key, cb) {
}); });
}; };
Storage.prototype.removeAllPendingTxs = function(walletId, cb) {
this._delByKey(KEY.PENDING_TXP(walletId), cb);
};
Storage.prototype.removeAllTxs = function(walletId, cb) {
this._delByKey(KEY.TXP(walletId), cb);
};
Storage.prototype._removeCopayers = function(walletId, cb) { Storage.prototype._removeCopayers = function(walletId, cb) {
var self = this; var self = this;
@ -333,15 +309,6 @@ Storage.prototype._removeCopayers = function(walletId, cb) {
}); });
}; };
Storage.prototype._removeAllNotifications = function(walletId, cb) {
this._delByKey(KEY.NOTIFICATION(walletId), cb);
};
Storage.prototype._removeAllAddresses = function(walletId, cb) {
this._delByKey(KEY.ADDRESS(walletId), cb);
};
Storage.prototype.removeWallet = function(walletId, cb) { Storage.prototype.removeWallet = function(walletId, cb) {
var self = this; var self = this;
@ -390,11 +357,6 @@ Storage.prototype.storeAddressAndWallet = function(wallet, address, cb) {
this.db.batch(ops, cb); this.db.batch(ops, cb);
}; };
Storage.prototype.removeAddress = function(walletId, address, cb) {
this.db.del(KEY.ADDRESS(walletId, address.address), cb);
};
Storage.prototype._dump = function(cb, fn) { Storage.prototype._dump = function(cb, fn) {
fn = fn || console.log; fn = fn || console.log;

88
test/integration/clientApi.js

@ -14,6 +14,7 @@ var Bitcore = require('bitcore');
var WalletUtils = require('../../lib/walletutils'); var WalletUtils = require('../../lib/walletutils');
var ExpressApp = require('../../lib/expressapp'); var ExpressApp = require('../../lib/expressapp');
var Storage = require('../../lib/storage'); var Storage = require('../../lib/storage');
var TestData = require('../testdata');
var helpers = {}; var helpers = {};
@ -86,10 +87,6 @@ fsmock._set = function(name, data) {
var blockExplorerMock = {}; var blockExplorerMock = {};
blockExplorerMock.utxos = [];
blockExplorerMock.getUnspentUtxos = function(dummy, cb) { blockExplorerMock.getUnspentUtxos = function(dummy, cb) {
var ret = _.map(blockExplorerMock.utxos || [], function(x) { var ret = _.map(blockExplorerMock.utxos || [], function(x) {
@ -112,16 +109,26 @@ blockExplorerMock.setUtxo = function(address, amount, m) {
}); });
}; };
blockExplorerMock.broadcast = function(raw, cb) { blockExplorerMock.broadcast = function(raw, cb) {
blockExplorerMock.lastBroadcasted = raw; blockExplorerMock.lastBroadcasted = raw;
return cb(null, (new Bitcore.Transaction(raw)).id); return cb(null, (new Bitcore.Transaction(raw)).id);
}; };
blockExplorerMock.setHistory = function(txs) {
blockExplorerMock.txHistory = txs;
};
blockExplorerMock.getTransactions = function(addresses, cb) {
return cb(null, blockExplorerMock.txHistory || []);
};
blockExplorerMock.reset = function() { blockExplorerMock.reset = function() {
blockExplorerMock.utxos = []; blockExplorerMock.utxos = [];
blockExplorerMock.txHistory = [];
}; };
describe('client API ', function() { describe('client API ', function() {
var clients, app; var clients, app;
@ -217,10 +224,8 @@ describe('client API ', function() {
done(); done();
}); });
}); });
}); });
describe('Storage Encryption', function() { describe('Storage Encryption', function() {
beforeEach(function() { beforeEach(function() {
_.each(_.range(3), function(i) { _.each(_.range(3), function(i) {
@ -326,7 +331,6 @@ describe('client API ', function() {
it.skip('should not ask for password if not needed (readwrite)', function(done) {}); it.skip('should not ask for password if not needed (readwrite)', function(done) {});
}); });
describe('Wallet Creation', function() { describe('Wallet Creation', function() {
it('should check balance in a 1-1 ', function(done) { it('should check balance in a 1-1 ', function(done) {
helpers.createAndJoinWallet(clients, 1, 1, function(err) { helpers.createAndJoinWallet(clients, 1, 1, function(err) {
@ -563,6 +567,7 @@ describe('client API ', function() {
}); });
}); });
}); });
describe('Air gapped related flows', function() { describe('Air gapped related flows', function() {
it('should be able get Tx proposals from a file', function(done) { it('should be able get Tx proposals from a file', function(done) {
helpers.createAndJoinWallet(clients, 1, 2, function(err, w) { helpers.createAndJoinWallet(clients, 1, 2, function(err, w) {
@ -823,10 +828,8 @@ describe('client API ', function() {
}); });
}); });
}); });
}); });
describe('Wallet Backups and Mobility', function() { describe('Wallet Backups and Mobility', function() {
it('round trip #import #export', function(done) { it('round trip #import #export', function(done) {
@ -869,9 +872,8 @@ describe('client API ', function() {
}); });
}); });
describe('Transaction Proposals Creation and Locked funds', function() { describe('Transaction Proposals Creation and Locked funds', function() {
it('Should lock and release funds', function(done) { it('Should lock and release funds through rejection', function(done) {
helpers.createAndJoinWallet(clients, 2, 2, function(err, w) { helpers.createAndJoinWallet(clients, 2, 2, function(err, w) {
clients[0].createAddress(function(err, x0) { clients[0].createAddress(function(err, x0) {
should.not.exist(err); should.not.exist(err);
@ -902,6 +904,37 @@ describe('client API ', function() {
}); });
}); });
}); });
it('Should lock and release funds through removal', function(done) {
helpers.createAndJoinWallet(clients, 2, 2, function(err, w) {
clients[0].createAddress(function(err, x0) {
should.not.exist(err);
should.exist(x0.address);
blockExplorerMock.setUtxo(x0, 1, 2);
blockExplorerMock.setUtxo(x0, 1, 2);
var opts = {
amount: 120000000,
toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5',
message: 'hello 1-1',
};
clients[0].sendTxProposal(opts, function(err, x) {
should.not.exist(err);
clients[0].sendTxProposal(opts, function(err, y) {
err.code.should.contain('INSUFFICIENTFUNDS');
clients[0].removeTxProposal(x, function(err) {
should.not.exist(err);
clients[0].sendTxProposal(opts, function(err, x) {
should.not.exist(err);
done();
});
});
});
});
});
});
});
it('Should keep message and refusal texts', function(done) { it('Should keep message and refusal texts', function(done) {
helpers.createAndJoinWallet(clients, 2, 3, function(err, w) { helpers.createAndJoinWallet(clients, 2, 3, function(err, w) {
clients[0].createAddress(function(err, x0) { clients[0].createAddress(function(err, x0) {
@ -1315,4 +1348,35 @@ describe('client API ', function() {
}); });
}); });
}); });
describe('Transaction history', function() {
it('should get transaction history', function(done) {
blockExplorerMock.setHistory(TestData.history);
helpers.createAndJoinWallet(clients, 1, 1, function(err, w) {
clients[0].createAddress(function(err, x0) {
should.not.exist(err);
should.exist(x0.address);
clients[0].getTxHistory({}, function(err, txs) {
should.not.exist(err);
should.exist(txs);
txs.length.should.equal(2);
done();
});
});
});
});
it('should get empty transaction history when there are no addresses', function(done) {
blockExplorerMock.setHistory(TestData.history);
helpers.createAndJoinWallet(clients, 1, 1, function(err, w) {
clients[0].getTxHistory({}, function(err, txs) {
should.not.exist(err);
should.exist(txs);
txs.length.should.equal(0);
done();
});
});
});
it.skip('should get transaction history decorated with proposal', function(done) {});
it.skip('should get paginated transaction history', function(done) {});
});
}); });

422
test/integration/server.js

@ -228,12 +228,11 @@ describe('Copay server', function() {
.privateKey .privateKey
.toString(); .toString();
var message = 'hola'; var sig = WalletUtils.signMessage('hello world', priv);
var sig = WalletUtils.signMessage(message, priv);
WalletService.getInstanceWithAuth({ WalletService.getInstanceWithAuth({
copayerId: wallet.copayers[0].id, copayerId: wallet.copayers[0].id,
message: message, message: 'hello world',
signature: sig, signature: sig,
}, function(err, server) { }, function(err, server) {
should.not.exist(err); should.not.exist(err);
@ -302,7 +301,7 @@ describe('Copay server', function() {
}; };
server.createWallet(opts, function(err, walletId) { server.createWallet(opts, function(err, walletId) {
should.not.exist(walletId); should.not.exist(walletId);
err.should.exist; should.exist(err);
err.message.should.contain('name'); err.message.should.contain('name');
done(); done();
}); });
@ -345,6 +344,21 @@ describe('Copay server', function() {
done(); done();
}); });
}); });
it('should fail to create wallet with invalid pubKey argument', function(done) {
var opts = {
name: 'my wallet',
m: 2,
n: 3,
pubKey: 'dummy',
};
server.createWallet(opts, function(err, walletId) {
should.not.exist(walletId);
should.exist(err);
err.message.should.contain('Invalid public key');
done();
});
});
}); });
describe('#joinWallet', function() { describe('#joinWallet', function() {
@ -397,7 +411,7 @@ describe('Copay server', function() {
}; };
server.joinWallet(copayerOpts, function(err, result) { server.joinWallet(copayerOpts, function(err, result) {
should.not.exist(result); should.not.exist(result);
err.should.exist; should.exist(err);
err.message.should.contain('name'); err.message.should.contain('name');
done(); done();
}); });
@ -451,6 +465,40 @@ describe('Copay server', function() {
}); });
}); });
it('should fail two wallets with same xPubKey', function(done) {
var copayerOpts = {
walletId: walletId,
name: 'me',
xPubKey: TestData.copayers[0].xPubKey,
xPubKeySignature: TestData.copayers[0].xPubKeySignature,
};
server.joinWallet(copayerOpts, function(err) {
should.not.exist(err);
var walletOpts = {
name: 'my other wallet',
m: 1,
n: 1,
pubKey: TestData.keyPair.pub,
};
server.createWallet(walletOpts, function(err, walletId) {
should.not.exist(err);
copayerOpts = {
walletId: walletId,
name: 'me',
xPubKey: TestData.copayers[0].xPubKey,
xPubKeySignature: TestData.copayers[0].xPubKeySignature,
};
server.joinWallet(copayerOpts, function(err) {
should.exist(err);
err.code.should.equal('CREGISTERED');
err.message.should.equal('Copayer ID already registered on server');
done();
});
});
});
});
it('should fail to join with bad formated signature', function(done) { it('should fail to join with bad formated signature', function(done) {
var copayerOpts = { var copayerOpts = {
walletId: walletId, walletId: walletId,
@ -471,7 +519,7 @@ describe('Copay server', function() {
xPubKey: TestData.copayers[0].xPubKey[0], xPubKey: TestData.copayers[0].xPubKey[0],
}; };
server.joinWallet(copayerOpts, function(err) { server.joinWallet(copayerOpts, function(err) {
err.should.exist; should.exist(err);
err.message.should.contain('argument missing'); err.message.should.contain('argument missing');
done(); done();
}); });
@ -579,7 +627,7 @@ describe('Copay server', function() {
it('should not create address if unable to store it', function(done) { it('should not create address if unable to store it', function(done) {
sinon.stub(server.storage, 'storeAddressAndWallet').yields('dummy error'); sinon.stub(server.storage, 'storeAddressAndWallet').yields('dummy error');
server.createAddress({}, function(err, address) { server.createAddress({}, function(err, address) {
err.should.exist; should.exist(err);
should.not.exist(address); should.not.exist(address);
server.getMainAddresses({}, function(err, addresses) { server.getMainAddresses({}, function(err, addresses) {
@ -621,7 +669,7 @@ describe('Copay server', function() {
helpers.getAuthServer(result.copayerId, function(server) { helpers.getAuthServer(result.copayerId, function(server) {
server.createAddress({}, function(err, address) { server.createAddress({}, function(err, address) {
should.not.exist(address); should.not.exist(address);
err.should.exist; should.exist(err);
err.message.should.contain('not complete'); err.message.should.contain('not complete');
done(); done();
}); });
@ -652,7 +700,7 @@ describe('Copay server', function() {
var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, null, TestData.copayers[0].privKey); var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, null, TestData.copayers[0].privKey);
server.createTx(txOpts, function(err, tx) { server.createTx(txOpts, function(err, tx) {
should.not.exist(tx); should.not.exist(tx);
err.should.exist; should.exist(err);
err.message.should.contain('not complete'); err.message.should.contain('not complete');
done(); done();
}); });
@ -710,7 +758,7 @@ describe('Copay server', function() {
server.createTx(txOpts, function(err, tx) { server.createTx(txOpts, function(err, tx) {
should.not.exist(tx); should.not.exist(tx);
err.should.exist; should.exist(err);
err.message.should.equal('Invalid proposal signature'); err.message.should.equal('Invalid proposal signature');
done(); done();
}); });
@ -723,7 +771,7 @@ describe('Copay server', function() {
server.createTx(txOpts, function(err, tx) { server.createTx(txOpts, function(err, tx) {
should.not.exist(tx); should.not.exist(tx);
err.should.exist; should.exist(err);
err.message.should.equal('Invalid proposal signature'); err.message.should.equal('Invalid proposal signature');
done(); done();
}); });
@ -736,7 +784,7 @@ describe('Copay server', function() {
server.createTx(txOpts, function(err, tx) { server.createTx(txOpts, function(err, tx) {
should.not.exist(tx); should.not.exist(tx);
err.should.exist; should.exist(err);
err.code.should.equal('INVALIDADDRESS'); err.code.should.equal('INVALIDADDRESS');
err.message.should.equal('Invalid address'); err.message.should.equal('Invalid address');
done(); done();
@ -758,6 +806,16 @@ describe('Copay server', function() {
}); });
}); });
it('should fail to create tx for invalid amount', function(done) {
var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0, null, TestData.copayers[0].privKey);
server.createTx(txOpts, function(err, tx) {
should.not.exist(tx);
should.exist(err);
err.message.should.equal('Invalid amount');
done();
});
});
it('should fail to create tx when insufficient funds', function(done) { it('should fail to create tx when insufficient funds', function(done) {
helpers.stubUtxos(server, wallet, [100], function() { helpers.stubUtxos(server, wallet, [100], function() {
var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 120, null, TestData.copayers[0].privKey); var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 120, null, TestData.copayers[0].privKey);
@ -803,6 +861,23 @@ describe('Copay server', function() {
}); });
}); });
it('should fail gracefully when bitcore throws exception on raw tx creation', function(done) {
helpers.stubUtxos(server, wallet, [10], function() {
var bitcoreStub = sinon.stub(Bitcore, 'Transaction');
bitcoreStub.throws({
name: 'dummy',
message: 'dummy exception'
});
var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 2, null, TestData.copayers[0].privKey);
server.createTx(txOpts, function(err, tx) {
should.exist(err);
err.message.should.equal('dummy exception');
bitcoreStub.restore();
done();
});
});
});
it('should create tx when there is a pending tx and enough UTXOs', function(done) { it('should create tx when there is a pending tx and enough UTXOs', function(done) {
helpers.stubUtxos(server, wallet, [10.1, 10.2, 10.3], function() { helpers.stubUtxos(server, wallet, [10.1, 10.2, 10.3], function() {
var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 12, null, TestData.copayers[0].privKey); var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 12, null, TestData.copayers[0].privKey);
@ -889,7 +964,7 @@ describe('Copay server', function() {
var server, wallet, txid; var server, wallet, txid;
beforeEach(function(done) { beforeEach(function(done) {
helpers.createAndJoinWallet(2, 3, function(s, w) { helpers.createAndJoinWallet(2, 2, function(s, w) {
server = s; server = s;
wallet = w; wallet = w;
helpers.stubUtxos(server, wallet, _.range(1, 9), function() { helpers.stubUtxos(server, wallet, _.range(1, 9), function() {
@ -916,20 +991,63 @@ describe('Copay server', function() {
should.not.exist(err); should.not.exist(err);
server.getPendingTxs({}, function(err, txs) { server.getPendingTxs({}, function(err, txs) {
should.not.exist(err); should.not.exist(err);
var tx = txs[0]; txs.should.be.empty;
tx.id.should.equal(txid); server.getTx({
txProposalId: txid
var actors = tx.getActors(); }, function(err, tx) {
actors.length.should.equal(1); var actors = tx.getActors();
actors[0].should.equal(wallet.copayers[0].id); actors.length.should.equal(1);
var action = tx.getActionBy(wallet.copayers[0].id); actors[0].should.equal(wallet.copayers[0].id);
action.type.should.equal('reject'); var action = tx.getActionBy(wallet.copayers[0].id);
action.comment.should.equal('some reason'); action.type.should.equal('reject');
done(); action.comment.should.equal('some reason');
done();
});
}); });
}); });
}); });
}); });
it('should fail to reject non-pending TX', function(done) {
async.waterfall([
function(next) {
server.getPendingTxs({}, function(err, txs) {
var tx = txs[0];
tx.id.should.equal(txid);
next();
});
},
function(next) {
server.rejectTx({
txProposalId: txid,
reason: 'some reason',
}, function(err) {
should.not.exist(err);
next();
});
},
function(next) {
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.should.be.empty;
next();
});
},
function(next) {
helpers.getAuthServer(wallet.copayers[1].id, function(server) {
server.rejectTx({
txProposalId: txid,
reason: 'some other reason',
}, function(err) {
should.exist(err);
err.code.should.equal('TXNOTPENDING');
done();
});
});
},
]);
});
}); });
describe('#signTx', function() { describe('#signTx', function() {
@ -1085,8 +1203,160 @@ describe('Copay server', function() {
}); });
}); });
}); });
it('should fail to sign a non-pending TX', function(done) {
async.waterfall([
function(next) {
server.rejectTx({
txProposalId: txid,
reason: 'some reason',
}, function(err) {
should.not.exist(err);
next();
});
},
function(next) {
helpers.getAuthServer(wallet.copayers[1].id, function(server) {
server.rejectTx({
txProposalId: txid,
reason: 'some reason',
}, function(err) {
should.not.exist(err);
next();
});
});
},
function(next) {
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.should.be.empty;
next();
});
},
function(next) {
helpers.getAuthServer(wallet.copayers[2].id, function(server) {
server.getTx({
txProposalId: txid
}, function(err, tx) {
should.not.exist(err);
var signatures = helpers.clientSign(tx, TestData.copayers[2].xPrivKey);
server.signTx({
txProposalId: txid,
signatures: signatures,
}, function(err) {
should.exist(err);
err.code.should.equal('TXNOTPENDING');
done();
});
});
});
},
]);
});
});
describe('#broadcastTx', function() {
var server, wallet, txpid;
beforeEach(function(done) {
helpers.createAndJoinWallet(1, 1, function(s, w) {
server = s;
wallet = w;
helpers.stubUtxos(server, wallet, [10, 10], function() {
var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 9, 'some message', TestData.copayers[0].privKey);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey);
server.signTx({
txProposalId: txp.id,
signatures: signatures,
}, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.isAccepted().should.be.true;
txp.isBroadcasted().should.be.false;
txpid = txp.id;
done();
});
});
});
});
});
it('should brodcast a tx', function(done) {
var clock = sinon.useFakeTimers(1234000);
helpers.stubBroadcast('999');
server.broadcastTx({
txProposalId: txpid
}, function(err) {
should.not.exist(err);
server.getTx({
txProposalId: txpid
}, function(err, txp) {
should.not.exist(err);
txp.txid.should.equal('999');
txp.isBroadcasted().should.be.true;
txp.broadcastedOn.should.equal(1234);
clock.restore();
done();
});
});
});
it('should fail to brodcast an already broadcasted tx', function(done) {
helpers.stubBroadcast('999');
server.broadcastTx({
txProposalId: txpid
}, function(err) {
should.not.exist(err);
server.broadcastTx({
txProposalId: txpid
}, function(err) {
should.exist(err);
err.code.should.equal('TXALREADYBROADCASTED');
done();
});
});
});
it('should fail to brodcast a not yet accepted tx', function(done) {
helpers.stubBroadcast('999');
var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 9, 'some other message', TestData.copayers[0].privKey);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
server.broadcastTx({
txProposalId: txp.id
}, function(err) {
should.exist(err);
err.code.should.equal('TXNOTACCEPTED');
done();
});
});
});
it('should keep tx as accepted if unable to broadcast it', function(done) {
helpers.stubBroadcastFail();
server.broadcastTx({
txProposalId: txpid
}, function(err) {
should.exist(err);
server.getTx({
txProposalId: txpid
}, function(err, txp) {
should.not.exist(err);
should.not.exist(txp.txid);
txp.isBroadcasted().should.be.false;
should.not.exist(txp.broadcastedOn);
txp.isAccepted().should.be.true;
done();
});
});
});
}); });
describe('Tx proposal workflow', function() { describe('Tx proposal workflow', function() {
var server, wallet; var server, wallet;
beforeEach(function(done) { beforeEach(function(done) {
@ -1104,7 +1374,7 @@ describe('Copay server', function() {
var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, 'some message', TestData.copayers[0].privKey); var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, 'some message', TestData.copayers[0].privKey);
server.createTx(txOpts, function(err, txp) { server.createTx(txOpts, function(err, txp) {
should.not.exist(err); should.not.exist(err);
should.exist.txp; should.exist(txp);
helpers.getAuthServer(wallet.copayers[1].id, function(server2, wallet) { helpers.getAuthServer(wallet.copayers[1].id, function(server2, wallet) {
server2.getPendingTxs({}, function(err, txps) { server2.getPendingTxs({}, function(err, txps) {
should.not.exist(err); should.not.exist(err);
@ -1126,7 +1396,7 @@ describe('Copay server', function() {
server.createTx(txOpts, function(err, txp) { server.createTx(txOpts, function(err, txp) {
txpId = txp.id; txpId = txp.id;
should.not.exist(err); should.not.exist(err);
should.exist.txp; should.exist(txp);
next(); next();
}); });
}, },
@ -1212,7 +1482,7 @@ describe('Copay server', function() {
server.createTx(txOpts, function(err, txp) { server.createTx(txOpts, function(err, txp) {
txpId = txp.id; txpId = txp.id;
should.not.exist(err); should.not.exist(err);
should.exist.txp; should.exist(txp);
next(); next();
}); });
}, },
@ -1269,7 +1539,7 @@ describe('Copay server', function() {
}, },
function(next) { function(next) {
server.getTx({ server.getTx({
id: txpId txProposalId: txpId
}, function(err, txp) { }, function(err, txp) {
should.not.exist(err); should.not.exist(err);
txp.isPending().should.be.false; txp.isPending().should.be.false;
@ -1283,6 +1553,48 @@ describe('Copay server', function() {
}); });
}); });
describe('#getTx', function() {
var server, wallet, txpid;
beforeEach(function(done) {
helpers.createAndJoinWallet(2, 3, function(s, w) {
server = s;
wallet = w;
helpers.stubUtxos(server, wallet, 10, function() {
var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 9, 'some message', TestData.copayers[0].privKey);
server.createTx(txOpts, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txpid = txp.id;
done();
});
});
});
});
it('should get own transaction proposal', function(done) {
server.getTx({
txProposalId: txpid
}, function(err, txp) {
should.not.exist(err);
should.exist(txp);
txp.id.should.equal(txpid);
done();
});
});
it.skip('should get someone elses transaction proposal', function(done) {});
it('should fail to get non-existent transaction proposal', function(done) {
server.getTx({
txProposalId: 'dummy'
}, function(err, txp) {
should.exist(err);
should.not.exist(txp);
err.message.should.contain('not found');
done();
});
});
it.skip('should get accepted/rejected transaction proposal', function(done) {});
it.skip('should get broadcasted transaction proposal', function(done) {});
});
describe('#getTxs', function() { describe('#getTxs', function() {
var server, wallet, clock; var server, wallet, clock;
@ -1623,9 +1935,10 @@ describe('Copay server', function() {
it('should allow creator to remove an signed TX by himself', function(done) { it('should allow creator to remove an signed TX by himself', function(done) {
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey); var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey);
server.signTx({ server.signTx({
txProposalId: txp[0], txProposalId: txp.id,
signatures: signatures, signatures: signatures,
}, function(err) { }, function(err) {
should.not.exist(err);
server.removePendingTx({ server.removePendingTx({
txProposalId: txp.id txProposalId: txp.id
}, function(err) { }, function(err) {
@ -1638,6 +1951,58 @@ describe('Copay server', function() {
}); });
}); });
it('should fail to remove non-pending TX', function(done) {
async.waterfall([
function(next) {
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey);
server.signTx({
txProposalId: txp.id,
signatures: signatures,
}, function(err) {
should.not.exist(err);
next();
});
},
function(next) {
helpers.getAuthServer(wallet.copayers[1].id, function(server) {
server.rejectTx({
txProposalId: txp.id,
}, function(err) {
should.not.exist(err);
next();
});
});
},
function(next) {
helpers.getAuthServer(wallet.copayers[2].id, function(server) {
server.rejectTx({
txProposalId: txp.id,
}, function(err) {
should.not.exist(err);
next();
});
});
},
function(next) {
server.getPendingTxs({}, function(err, txs) {
should.not.exist(err);
txs.should.be.empty;
next();
});
},
function(next) {
server.removePendingTx({
txProposalId: txp.id
}, function(err) {
should.exist(err);
err.code.should.equal('TXNOTPENDING');
done();
});
},
]);
});
it('should not allow non-creator copayer to remove an unsigned TX ', function(done) { it('should not allow non-creator copayer to remove an unsigned TX ', function(done) {
helpers.getAuthServer(wallet.copayers[1].id, function(server2) { helpers.getAuthServer(wallet.copayers[1].id, function(server2) {
server2.removePendingTx({ server2.removePendingTx({
@ -1663,6 +2028,7 @@ describe('Copay server', function() {
server.removePendingTx({ server.removePendingTx({
txProposalId: txp.id txProposalId: txp.id
}, function(err) { }, function(err) {
err.code.should.equal('TXACTIONED');
err.message.should.contain('other copayers'); err.message.should.contain('other copayers');
done(); done();
}); });

2
test/addressmanager.js → test/models/addressmanager.js

@ -4,7 +4,7 @@ var _ = require('lodash');
var chai = require('chai'); var chai = require('chai');
var sinon = require('sinon'); var sinon = require('sinon');
var should = chai.should(); var should = chai.should();
var AddressManager = require('../lib/model/addressmanager'); var AddressManager = require('../../lib/model/addressmanager');
describe('AddressManager', function() { describe('AddressManager', function() {

2
test/txproposal.js → test/models/txproposal.js

@ -4,7 +4,7 @@ var _ = require('lodash');
var chai = require('chai'); var chai = require('chai');
var sinon = require('sinon'); var sinon = require('sinon');
var should = chai.should(); var should = chai.should();
var TXP = require('../lib/model/txproposal'); var TXP = require('../../lib/model/txproposal');
var Bitcore = require('bitcore'); var Bitcore = require('bitcore');

2
test/wallet.js → test/models/wallet.js

@ -4,7 +4,7 @@ var _ = require('lodash');
var chai = require('chai'); var chai = require('chai');
var sinon = require('sinon'); var sinon = require('sinon');
var should = chai.should(); var should = chai.should();
var Wallet = require('../lib/model/wallet'); var Wallet = require('../../lib/model/wallet');
describe('Wallet', function() { describe('Wallet', function() {

51
test/utils.js

@ -0,0 +1,51 @@
'use strict';
var _ = require('lodash');
var chai = require('chai');
var sinon = require('sinon');
var should = chai.should();
var Utils = require('../lib/utils');
describe('Utils', function() {
describe('#checkRequired', function() {
it('should check required fields', function() {
var obj = {
id: 'id',
name: 'name',
array: ['a', 'b'],
};
var fixtures = [{
args: 'id',
check: true
}, {
args: ['id'],
check: true
}, {
args: ['id, name'],
check: false
}, {
args: ['id', 'name'],
check: true
}, {
args: 'array',
check: true
}, {
args: 'dummy',
check: false
}, {
args: ['dummy1', 'dummy2'],
check: false
}, {
args: ['id', 'dummy'],
check: false
}, ];
_.each(fixtures, function(f) {
Utils.checkRequired(obj, f.args).should.equal(f.check);
});
});
it('should fail to check required fields on non-object', function() {
var obj = 'dummy';
Utils.checkRequired(obj, 'name').should.be.false;
});
});
});
Loading…
Cancel
Save