Improved tests

This commit is contained in:
Ivan Socolsky 2014-09-29 17:36:34 -03:00
commit 2c53bc073e
4 changed files with 281 additions and 142 deletions

View file

@ -129,7 +129,7 @@ TxProposal.fromObj = function(o, forceOpts) {
forceOpts = forceOpts || {}; forceOpts = forceOpts || {};
if (forceOpts){ if (forceOpts) {
o.builderObj.opts = o.builderObj.opts || {}; o.builderObj.opts = o.builderObj.opts || {};
} }
@ -232,6 +232,10 @@ TxProposal.prototype.mergeBuilder = function(incoming) {
}; };
TxProposal.prototype.getSeen = function(copayerId) {
return this.seenBy[copayerId];
};
TxProposal.prototype.setSeen = function(copayerId) { TxProposal.prototype.setSeen = function(copayerId) {
if (!this.seenBy[copayerId]) if (!this.seenBy[copayerId])
this.seenBy[copayerId] = Date.now(); this.seenBy[copayerId] = Date.now();

View file

@ -341,7 +341,6 @@ Wallet.prototype._checkSentTx = function(ntxid, cb) {
this.blockchain.getTransaction(txid, function(err, tx) { this.blockchain.getTransaction(txid, function(err, tx) {
if (err) return cb(false); if (err) return cb(false);
txp.setSent(tx.txid);
cb(ret); cb(ret);
}); });
}; };
@ -370,21 +369,26 @@ Wallet.prototype._onTxProposal = function(senderId, data) {
} }
if (m) { if (m) {
if (m.hasChanged) { if (!m.txp.getSeen(this.getMyCopayerId())) {
m.txp.setSeen(this.getMyCopayerId()); m.txp.setSeen(this.getMyCopayerId());
this.sendSeen(m.ntxid); this.sendSeen(m.ntxid);
var tx = m.txp.builder.build(); }
if (tx.isComplete()) {
this._checkSentTx(m.ntxid, function(ret) { var tx = m.txp.builder.build();
if (ret) { if (tx.isComplete()) {
self.emit('txProposalsUpdated'); this._checkSentTx(m.ntxid, function(ret) {
self.store(); if (ret) {
} m.txp.setSent(m.mtxid);
}); self.emit('txProposalsUpdated');
} else { self.store();
}
});
} else {
if (m.hasChanged) {
this.sendTxProposal(m.ntxid); this.sendTxProposal(m.ntxid);
} }
} }
this.emit('txProposalsUpdated'); this.emit('txProposalsUpdated');
this.store(); this.store();
} }
@ -1125,9 +1129,8 @@ Wallet.prototype.getTxProposals = function() {
var txp = this.txProposals.getTxProposal(ntxid, copayers); var txp = this.txProposals.getTxProposal(ntxid, copayers);
txp.signedByUs = txp.signedBy[this.getMyCopayerId()] ? true : false; txp.signedByUs = txp.signedBy[this.getMyCopayerId()] ? true : false;
txp.rejectedByUs = txp.rejectedBy[this.getMyCopayerId()] ? true : false; txp.rejectedByUs = txp.rejectedBy[this.getMyCopayerId()] ? true : false;
if (this.totalCopayers - txp.rejectCount < this.requiredCopayers) { txp.finallyRejected = this.totalCopayers - txp.rejectCount < this.requiredCopayers;
txp.finallyRejected = true; txp.isPending = !txp.finallyRejected && !txp.sentTxid;
}
if (!txp.readonly || txp.finallyRejected || txp.sentTs) { if (!txp.readonly || txp.finallyRejected || txp.sentTs) {
ret.push(txp); ret.push(txp);
@ -1423,9 +1426,7 @@ Wallet.prototype.receivePaymentRequest = function(options, pr, cb) {
expires: expires, expires: expires,
memo: memo || 'This server would like some BTC from you.', memo: memo || 'This server would like some BTC from you.',
payment_url: payment_url, payment_url: payment_url,
merchant_data: merchant_data merchant_data: merchant_data ? merchant_data.toString('hex') : null
? merchant_data.toString('hex')
: null
}, },
signature: sig.toString('hex'), signature: sig.toString('hex'),
ca: trust.caName, ca: trust.caName,
@ -2106,41 +2107,34 @@ Wallet.prototype.removeTxWithSpentInputs = function(cb) {
cb = cb || function() {}; cb = cb || function() {};
var txps = []; var txps = _.where(this.getTxProposals(), {
var maxRejectCount = this.maxRejectCount(); isPending: true
for (var ntxid in this.txProposals.txps) { });
var txp = this.txProposals.txps[ntxid]; var inputs = _.flatten(_.map(txps, function(txp) {
txp.ntxid = ntxid; return _.map(txp.builder.utxos, function(utxo) {
if (txp.isPending(maxRejectCount)) { return {
txps.push(txp);
}
}
var inputs = [];
txps.forEach(function(txp) {
txp.builder.utxos.forEach(function(utxo) {
inputs.push({
ntxid: txp.ntxid, ntxid: txp.ntxid,
txid: utxo.txid, txid: utxo.txid,
vout: utxo.vout vout: utxo.vout,
}); };
}); });
}); }));
if (inputs.length === 0) if (inputs.length === 0)
return; return cb();
var proposalsChanged = false; var proposalsChanged = false;
this.blockchain.getUnspent(this.getAddressesStr(), function(err, unspentList) { this.blockchain.getUnspent(this.getAddressesStr(), function(err, unspentList) {
if (err) return cb(err); if (err) return cb(err);
unspentList.forEach(function(unspent) { _.each(unspentList, function(unspent) {
inputs.forEach(function(input) { _.each(inputs, function(input) {
input.unspent = input.unspent || (input.txid === unspent.txid && input.vout === unspent.vout); input.unspent = input.unspent || (input.txid === unspent.txid && input.vout === unspent.vout);
}); });
}); });
inputs.forEach(function(input) { _.each(inputs, function(input) {
if (!input.unspent) { if (!input.unspent) {
proposalsChanged = true; proposalsChanged = true;
self.txProposals.deleteOne(input.ntxid); self.txProposals.deleteOne(input.ntxid);
@ -2152,7 +2146,7 @@ Wallet.prototype.removeTxWithSpentInputs = function(cb) {
self.store(); self.store();
} }
cb(null); return cb();
}); });
}; };

View file

@ -122,7 +122,7 @@ angular.module('copayApp.services')
notification.info('Transaction Update', $filter('translate')('A transaction was rejected by') + ' ' + user); notification.info('Transaction Update', $filter('translate')('A transaction was rejected by') + ' ' + user);
break; break;
case 'corrupt': case 'corrupt':
notification.error('Transaction Error', $filter('translate')('Received corrupt transaction from') + ' ' + user); notification.error('Transaction Error', $filter('translate')('Received corrupt transaction from') + ' ' + user);
break; break;
} }
}); });
@ -177,8 +177,6 @@ angular.module('copayApp.services')
if (!w) return root.onErrorDigest(); if (!w) return root.onErrorDigest();
if (!w.isReady()) return; if (!w.isReady()) return;
w.removeTxWithSpentInputs();
$rootScope.balanceByAddr = {}; $rootScope.balanceByAddr = {};
$rootScope.updatingBalance = true; $rootScope.updatingBalance = true;
@ -232,12 +230,9 @@ angular.module('copayApp.services')
return txs.push(null); return txs.push(null);
} }
if (myCopayerId != i.creator && !i.finallyRejected && !i.sentTs && !i.rejectedByUs && !i.signedByUs) { if (i.isPending && myCopayerId != i.creator && !i.rejectedByUs && !i.signedByUs) {
pendingForUs++; pendingForUs++;
} }
if (!i.finallyRejected && !i.sentTs) {
i.isPending = 1;
}
if (!!opts.pending == !!i.isPending) { if (!!opts.pending == !!i.isPending) {
var tx = i.builder.build(); var tx = i.builder.build();
@ -262,6 +257,8 @@ angular.module('copayApp.services')
} }
}); });
w.removeTxWithSpentInputs();
$rootScope.txs = txs; $rootScope.txs = txs;
$rootScope.txsOpts = opts; $rootScope.txsOpts = opts;
if ($rootScope.pendingTxCount < pendingForUs) { if ($rootScope.pendingTxCount < pendingForUs) {

View file

@ -864,76 +864,76 @@ describe('Wallet model', function() {
}); });
describe('removeTxWithSpentInputs', function() { describe('removeTxWithSpentInputs', function() {
var w;
var utxos;
beforeEach(function() {
w = cachedCreateW2();
w.txProposals.deleteOne = sinon.spy();
utxos = [{
txid: 'txid0',
vout: 'vout1',
}, {
txid: 'txid0',
vout: 'vout2',
}];
});
it('should remove pending TxProposal with spent inputs', function(done) { it('should remove pending TxProposal with spent inputs', function(done) {
var w = cachedCreateW2(); var txp = {
var utxo = createUTXO(w); ntxid: 'txid1',
chai.expect(w.getTxProposals().length).to.equal(0); isPending: true,
w.blockchain.fixUnspent(utxo); builder: {
w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { utxos: [utxos[0]],
w.sendTxProposal(ntxid); }
chai.expect(w.getTxProposals().length).to.equal(1); };
w.getTxProposals = sinon.stub().returns([txp]);
// Inputs are still available, txp still valid w.blockchain.getUnspent = sinon.stub().yields(null, utxos);
w.removeTxWithSpentInputs(); w.removeTxWithSpentInputs(function() {
chai.expect(w.getTxProposals().length).to.equal(1); w.txProposals.deleteOne.called.should.be.false;
w.blockchain.getUnspent = sinon.stub().yields(null, []);
// Simulate input spent. txp should be removed from txps list w.removeTxWithSpentInputs(function() {
w.blockchain.fixUnspent([]); w.txProposals.deleteOne.calledWith('txid1').should.be.true;
w.removeTxWithSpentInputs(); done();
chai.expect(w.getTxProposals().length).to.equal(0); });
done();
}); });
}); });
it('should remove pending TxProposal with at least 1 spent input', function(done) { it('should remove pending TxProposal with at least 1 spent input', function(done) {
var w = cachedCreateW2(); var txp = {
var utxo = [createUTXO(w)[0], createUTXO(w)[0]]; ntxid: 'txid1',
utxo[0].amount = 80000; isPending: true,
utxo[1].amount = 80000; builder: {
utxo[1].vout = 1; utxos: utxos,
chai.expect(w.getTxProposals().length).to.equal(0); }
w.blockchain.fixUnspent(utxo); };
w.createTx(toAddress, '100000', null, function(err, ntxid) { w.getTxProposals = sinon.stub().returns([txp]);
w.sendTxProposal(ntxid); w.blockchain.getUnspent = sinon.stub().yields(null, utxos);
chai.expect(w.getTxProposals().length).to.equal(1); w.removeTxWithSpentInputs(function() {
w.txProposals.deleteOne.called.should.be.false;
// Inputs are still available, txp still valid w.blockchain.getUnspent = sinon.stub().yields(null, [utxos[0]]);
w.removeTxWithSpentInputs(); w.removeTxWithSpentInputs(function() {
chai.expect(w.getTxProposals().length).to.equal(1); w.txProposals.deleteOne.calledWith('txid1').should.be.true;
done();
// Simulate 1 input spent. txp should be removed from txps list });
w.blockchain.fixUnspent([utxo[0]]);
w.removeTxWithSpentInputs();
chai.expect(w.getTxProposals().length).to.equal(0);
done();
}); });
}); });
it('should not remove complete TxProposal', function(done) { it('should not remove complete TxProposal', function(done) {
var w = cachedCreateW2(); var txp = {
var utxo = createUTXO(w); ntxid: 'txid1',
chai.expect(w.getTxProposals().length).to.equal(0); isPending: false,
w.blockchain.fixUnspent(utxo); builder: {
w.createTx(toAddress, amountSatStr, null, function(err, ntxid) { utxos: [utxos[0]],
w.sendTxProposal(ntxid); }
chai.expect(w.getTxProposals().length).to.equal(1); };
w.getTxProposals = sinon.stub().returns([txp]);
// Inputs are still available, txp still valid w.blockchain.getUnspent = sinon.stub().yields(null, utxos);
w.removeTxWithSpentInputs(); w.removeTxWithSpentInputs(function() {
chai.expect(w.getTxProposals().length).to.equal(1); w.txProposals.deleteOne.called.should.be.false;
w.blockchain.getUnspent = sinon.stub().yields(null, []);
// Simulate input spent. txp should be removed from txps list w.removeTxWithSpentInputs(function() {
w.blockchain.fixUnspent([]); w.txProposals.deleteOne.called.should.be.false;
var txp = w.txProposals.get(ntxid); done();
sinon.stub(txp, 'isPending', function() { });
return false;
})
w.removeTxWithSpentInputs();
chai.expect(w.getTxProposals().length).to.equal(1);
done();
}); });
}); });
}); });
@ -1425,60 +1425,204 @@ describe('Wallet model', function() {
describe('_onTxProposal', function() { describe('_onTxProposal', function() {
var testValidate = function(response, result, done) { var w;
beforeEach(function() {
w = cachedCreateW();
w._getKeyMap = sinon.stub();
w.sendSeen = sinon.spy();
w.sendTxProposal = sinon.spy();
});
it('should handle corrupt tx', function(done) {
var data = {
txProposal: {
dummy: 1,
},
};
w.txProposals.merge = sinon.stub().throws(new Error('test error'));
var w = cachedCreateW();
var spy = sinon.spy(); var spy = sinon.spy();
w.on('txProposalEvent', spy); w.on('txProposalEvent', spy);
w.on('txProposalEvent', function(e) { w.on('txProposalEvent', function(e) {
e.type.should.equal(result); e.type.should.equal('corrupt');
done(); done();
}); });
// txp.prototype.getId = function() {return 'aa'};
var txp = {
dummy: 1
};
var txp = {
'txProposal': txp
};
var s1 = sinon.stub(w, '_getKeyMap', function() { w._onTxProposal('senderID', data);
return { spy.called.should.be.true;
1: 2
};
});
var s2 = sinon.stub(w.txProposals, 'merge', function() {
if (response == 0)
throw new Error('test error');
return {
ntxid: 1,
txp: {
setCopayers: function() {
return ['oeoe'];
},
},
new: response == 1
};
});
w._onTxProposal('senderID', txp);
spy.callCount.should.equal(1);
s1.restore();
s2.restore();
};
it('should handle corrupt', function(done) {
testValidate(0, 'corrupt', done);
}); });
it('should handle new', function(done) { it('should handle new', function(done) {
testValidate(1, 'new', done); var data = {
}); txProposal: {
it('should handle signed', function(done) { dummy: 1,
testValidate(2, 'signed', done); },
};
var txp = {
getSeen: sinon.stub().returns(false),
setSeen: sinon.spy(),
setCopayers: sinon.spy(),
builder: {
build: sinon.stub().returns({
isComplete: sinon.stub().returns(false),
}),
},
};
w.txProposals.merge = sinon.stub().returns({
ntxid: 1,
txp: txp,
new: true,
hasChanged: true,
});
var spy1 = sinon.spy();
var spy2 = sinon.spy();
w.on('txProposalEvent', spy1);
w.on('txProposalsUpdated', spy2);
w.on('txProposalEvent', function(e) {
e.type.should.equal('new');
done();
});
w._onTxProposal('senderID', data);
spy1.called.should.be.true;
spy2.called.should.be.true;
txp.setSeen.calledOnce.should.be.true;
w.sendSeen.calledOnce.should.be.true;
w.sendTxProposal.calledOnce.should.be.true;
}); });
it('should handle signed', function(done) {
var data = {
txProposal: {
dummy: 1,
},
};
var txp = {
getSeen: sinon.stub().returns(true),
setSeen: sinon.spy(),
setCopayers: sinon.stub().returns(['new copayer']),
builder: {
build: sinon.stub().returns({
isComplete: sinon.stub().returns(false),
}),
},
};
w.txProposals.merge = sinon.stub().returns({
ntxid: 1,
txp: txp,
new: false,
hasChanged: true,
});
var spy1 = sinon.spy();
var spy2 = sinon.spy();
w.on('txProposalEvent', spy1);
w.on('txProposalsUpdated', spy2);
w.on('txProposalEvent', function(e) {
e.type.should.equal('signed');
done();
});
w._onTxProposal('senderID', data);
spy1.called.should.be.true;
spy2.called.should.be.true;
txp.setSeen.calledOnce.should.be.false;
w.sendSeen.calledOnce.should.be.false;
w.sendTxProposal.calledOnce.should.be.true;
});
it('should mark as broadcast when complete', function(done) {
var data = {
txProposal: {
dummy: 1,
},
};
var txp = {
getSeen: sinon.stub().returns(true),
setCopayers: sinon.stub().returns(['new copayer']),
setSent: sinon.spy(),
builder: {
build: sinon.stub().returns({
isComplete: sinon.stub().returns(true),
}),
},
};
w.txProposals.merge = sinon.stub().returns({
ntxid: 1,
txp: txp,
new: false,
hasChanged: true,
});
w._checkSentTx = sinon.stub().yields(true);
w._onTxProposal('senderID', data);
txp.setSent.calledOnce.should.be.true;
w.sendTxProposal.called.should.be.false;
done();
});
it('should only mark as broadcast if found in the blockchain', function(done) {
var data = {
txProposal: {
dummy: 1,
},
};
var txp = {
getSeen: sinon.stub().returns(true),
setCopayers: sinon.stub().returns(['new copayer']),
setSent: sinon.spy(),
builder: {
build: sinon.stub().returns({
isComplete: sinon.stub().returns(true),
}),
},
};
w.txProposals.merge = sinon.stub().returns({
ntxid: 1,
txp: txp,
new: false,
hasChanged: true,
});
w._checkSentTx = sinon.stub().yields(false);
w._onTxProposal('senderID', data);
txp.setSent.called.should.be.false;
w.sendTxProposal.called.should.be.false;
done();
});
it('should resend when not complete only if changed', function(done) {
var data = {
txProposal: {
dummy: 1,
},
};
var txp = {
getSeen: sinon.stub().returns(true),
setCopayers: sinon.stub().returns(['new copayer']),
builder: {
build: sinon.stub().returns({
isComplete: sinon.stub().returns(false),
}),
},
};
w.txProposals.merge = sinon.stub().returns({
ntxid: 1,
txp: txp,
new: false,
hasChanged: false,
});
w._onTxProposal('senderID', data);
w.sendTxProposal.called.should.be.false;
done();
});
}); });