From 59f562a6ebf317ecca85b94ec512056816841ea6 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Fri, 20 Feb 2015 11:23:28 -0300 Subject: [PATCH 1/5] add fake address tests --- test/integration/clientApi.js | 146 +++++++++++++++++++--------------- 1 file changed, 81 insertions(+), 65 deletions(-) diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index e086bd0..35f84b5 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -202,7 +202,7 @@ describe('client API ', function() { should.not.exist(err); // Get right response - var data = clients[0]._load(function(err, data) { + clients[0]._load(function(err, data) { var url = '/v1/wallets/'; clients[0]._doGetRequest(url, data, function(err, x) { @@ -316,6 +316,57 @@ describe('client API ', function() { }); }); }); + it('should detect fake addresses', function(done) { + helpers.createAndJoinWallet(clients, 2, 2, function(err) { + should.not.exist(err); + + // Get right response + clients[0]._load(function(err, data) { + var url = '/v1/addresses/'; + clients[0]._doPostRequest(url, {}, data, function(err, address) { + + // Tamper data + address.address = '2N86pNEpREGpwZyHVC5vrNUCbF9nM1Geh4K'; + + // Tamper response + clients[1]._doPostRequest = sinon.stub().yields(null, address); + + // Grab real response + clients[1].createAddress(function(err, x0) { + err.code.should.contain('SERVERCOMPROMISED'); + done(); + }); + }); + }); + }); + }); + it('should detect fake public keys', function(done) { + helpers.createAndJoinWallet(clients, 2, 2, function(err) { + should.not.exist(err); + + // Get right response + clients[0]._load(function(err, data) { + var url = '/v1/addresses/'; + clients[0]._doPostRequest(url, {}, data, function(err, address) { + console.log('[clientApi.js.326:address:]', address); //TODO + + // Tamper data + address.publicKeys = ['0322defe0c3eb9fcd8bc01878e6dbca7a6846880908d214b50a752445040cc5c54', + '02bf3aadc17131ca8144829fa1883c1ac0a8839067af4bca47a90ccae63d0d8037']; + + // Tamper response + clients[1]._doPostRequest = sinon.stub().yields(null, address); + + // Grab real response + clients[1].createAddress(function(err, x0) { + err.code.should.contain('SERVERCOMPROMISED'); + done(); + }); + }); + }); + }); + }); + }); @@ -524,9 +575,11 @@ describe('client API ', function() { }); }); }); + + }); - describe('Transaction proposals and locked funds', function() { + describe('Send Transaction Troposals and Locked funds', function() { it('Should lock and release funds', function(done) { helpers.createAndJoinWallet(clients, 2, 2, function(err, w) { clients[0].createAddress(function(err, x0) { @@ -558,76 +611,39 @@ describe('client API ', function() { }); }); }); + it('Should keep message and refusal texts', function(done) { + var msg = 'abcdefg'; + helpers.createAndJoinWallet(clients, 2, 3, function(err, w) { + clients[0].createAddress(function(err, x0) { + should.not.exist(err); + blockExplorerMock.setUtxo(x0, 10, 2); + var opts = { + amount: 10000, + toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', + message: msg, + }; + clients[0].sendTxProposal(opts, function(err, x) { + should.not.exist(err); + clients[1].rejectTxProposal(x, 'xx', function(err, tx1) { + should.not.exist(err); + clients[2].getTxProposals({}, function(err, txs) { + should.not.exist(err); + txs[0].decryptedMessage.should.equal(msg); + _.values(txs[0].actions)[0].comment.should.equal('xx'); + done(); + }); + }); + }); + }); + }); + }); }); /* - describe('TODO', function(x) { - it('should detect fake addresses ', function(done) { - var response = { - createdOn: 1424105995, - address: '2N3fA6wDtnebzywPkGuNK9KkFaEzgbPRRTq', - path: 'm/2147483647/0/8', - publicKeys: ['03f6a5fe8db51bfbaf26ece22a3e3bc242891a47d3048fc70bc0e8c03a071ad76f'] - }; - var request = sinon.mock().yields(null, { - statusCode: 200 - }, response); - client.request = request; - client.createAddress(function(err, x) { - err.code.should.equal('SERVERCOMPROMISED'); - err.message.should.contain('fake address'); - 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)); - var request = sinon.mock().yields(null, { - statusCode: 200 - }, TestData.serverResponse.pendingTxs); - client.request = request; - - client.getTxProposals({}, function(err, x) { - should.not.exist(err); - x.length.should.equal(1); - x[0].id.should.equal(TestData.serverResponse.pendingTxs[0].id); - x[0].decryptedMessage.should.equal('hola'); - 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)); - var response = {}; - var request = sinon.mock().yields(null, { - statusCode: 200 - }, response); - client.request = request; - - var args = { - toAddress: '2N3fA6wDtnebzywPkGuNK9KkFaEzgbPRRTq', - amount: '200bit', - message: 'some message', - }; - client.sendTxProposal(args, function(err) { - var callArgs = request.getCall(0).args[0].body; - callArgs.toAddress.should.equal(args.toAddress); - callArgs.amount.should.equal(20000); - callArgs.message.should.not.equal(args.message); - var decryptedMsg = WalletUtils.decryptMessage(callArgs.message, TestData.storage.complete11.sharedEncryptingKey); - decryptedMsg.should.equal(args.message); - done(); - }); - }); - }); - describe('#signTxProposal ', function() { it.skip('should sign tx proposal', function(done) {}); From 5804ca44568bdcd9eb0275daafe1497ef3b23dc0 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Fri, 20 Feb 2015 11:52:01 -0300 Subject: [PATCH 2/5] add fake tx proposal tests --- lib/client/Verifier.js | 3 +- lib/client/api.js | 8 + test/integration/clientApi.js | 295 +++++++++++++++++++++------------- 3 files changed, 191 insertions(+), 115 deletions(-) diff --git a/lib/client/Verifier.js b/lib/client/Verifier.js index 639b73e..cb891b6 100644 --- a/lib/client/Verifier.js +++ b/lib/client/Verifier.js @@ -64,7 +64,8 @@ Verifier.checkTxProposal = function(data, txp) { var hash = WalletUtils.getProposalHash(txp.toAddress, txp.amount, txp.message); log.debug('Regenerating & verifying tx proposal hash -> Hash: ', hash, ' Signature: ', txp.proposalSignature); - if (!WalletUtils.verifyMessage(hash, txp.proposalSignature, creatorSigningPubKey)) return false; + if (!WalletUtils.verifyMessage(hash, txp.proposalSignature, creatorSigningPubKey)) + return false; return Verifier.checkAddress(data, txp.changeAddress); }; diff --git a/lib/client/api.js b/lib/client/api.js index 31a195e..32f6d05 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -439,10 +439,18 @@ API.prototype.getTxProposals = function(opts, cb) { var url = '/v1/txproposals/'; self._doGetRequest(url, data, function(err, txps) { if (err) return cb(err); + var fake = false; _.each(txps, function(txp) { txp.decryptedMessage = _decryptProposalMessage(txp.message, data.sharedEncryptingKey); + + if (!Verifier.checkTxProposal(data, txp)) + fake = true; }); + + if (fake) + return cb(new ServerCompromisedError('Server sent fake transaction proposal')); + return cb(null, txps); }); }); diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index 35f84b5..fcd9f05 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -351,8 +351,9 @@ describe('client API ', function() { console.log('[clientApi.js.326:address:]', address); //TODO // Tamper data - address.publicKeys = ['0322defe0c3eb9fcd8bc01878e6dbca7a6846880908d214b50a752445040cc5c54', - '02bf3aadc17131ca8144829fa1883c1ac0a8839067af4bca47a90ccae63d0d8037']; + address.publicKeys = ['0322defe0c3eb9fcd8bc01878e6dbca7a6846880908d214b50a752445040cc5c54', + '02bf3aadc17131ca8144829fa1883c1ac0a8839067af4bca47a90ccae63d0d8037' + ]; // Tamper response clients[1]._doPostRequest = sinon.stub().yields(null, address); @@ -413,6 +414,184 @@ describe('client API ', function() { }); + describe('Transaction Troposals Creation and Locked funds', function() { + it('Should lock and release funds', 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: 'hola 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].rejectTxProposal(x, 'no', function(err, z) { + should.not.exist(err); + z.status.should.equal('rejected'); + clients[0].sendTxProposal(opts, function(err, x) { + should.not.exist(err); + done(); + }); + }); + }); + }); + }); + }); + }); + it('Should keep message and refusal texts', function(done) { + var msg = 'abcdefg'; + helpers.createAndJoinWallet(clients, 2, 3, function(err, w) { + clients[0].createAddress(function(err, x0) { + should.not.exist(err); + blockExplorerMock.setUtxo(x0, 10, 2); + var opts = { + amount: 10000, + toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', + message: msg, + }; + clients[0].sendTxProposal(opts, function(err, x) { + should.not.exist(err); + clients[1].rejectTxProposal(x, 'xx', function(err, tx1) { + should.not.exist(err); + clients[2].getTxProposals({}, function(err, txs) { + should.not.exist(err); + txs[0].decryptedMessage.should.equal(msg); + _.values(txs[0].actions)[0].comment.should.equal('xx'); + done(); + }); + }); + }); + }); + }); + }); + it('should detect fake tx proposals (wrong signature)', function(done) { + helpers.createAndJoinWallet(clients, 2, 2, function(err) { + should.not.exist(err); + + clients[0].createAddress(function(err, x0) { + should.not.exist(err); + blockExplorerMock.setUtxo(x0, 10, 2); + var opts = { + amount: 10000, + toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', + message: 'hola', + }; + clients[0].sendTxProposal(opts, function(err, x) { + should.not.exist(err); + + + // Get right response + clients[0]._load(function(err, data) { + var url = '/v1/txproposals/'; + clients[0]._doGetRequest(url, data, function(err, txps) { + + // Tamper data + txps[0].proposalSignature = '304402206e4a1db06e00068582d3be41cfc795dcf702451c132581e661e7241ef34ca19202203e17598b4764913309897d56446b51bc1dcd41a25d90fdb5f87a6b58fe3a6920'; + + // Tamper response + clients[0]._doGetRequest = sinon.stub().yields(null, txps); + + // Grab real response + clients[0].getTxProposals({}, function(err, txps) { + should.exist(err); + err.code.should.contain('SERVERCOMPROMISED'); + done(); + }); + }); + }); + }); + }); + }); + }); + it('should detect fake tx proposals (tampered amount)', function(done) { + helpers.createAndJoinWallet(clients, 2, 2, function(err) { + should.not.exist(err); + + clients[0].createAddress(function(err, x0) { + should.not.exist(err); + blockExplorerMock.setUtxo(x0, 10, 2); + var opts = { + amount: 10000, + toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', + message: 'hola', + }; + clients[0].sendTxProposal(opts, function(err, x) { + should.not.exist(err); + + + // Get right response + clients[0]._load(function(err, data) { + var url = '/v1/txproposals/'; + clients[0]._doGetRequest(url, data, function(err, txps) { + + // Tamper data + txps[0].amount = 100000; + + // Tamper response + clients[0]._doGetRequest = sinon.stub().yields(null, txps); + + // Grab real response + clients[0].getTxProposals({}, function(err, txps) { + should.exist(err); + err.code.should.contain('SERVERCOMPROMISED'); + done(); + }); + }); + }); + }); + }); + }); + }); + it('should detect fake tx proposals (change address not it wallet)', function(done) { + helpers.createAndJoinWallet(clients, 2, 2, function(err) { + should.not.exist(err); + + clients[0].createAddress(function(err, x0) { + should.not.exist(err); + blockExplorerMock.setUtxo(x0, 10, 2); + var opts = { + amount: 10000, + toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', + message: 'hola', + }; + clients[0].sendTxProposal(opts, function(err, x) { + should.not.exist(err); + + + // Get right response + clients[0]._load(function(err, data) { + var url = '/v1/txproposals/'; + clients[0]._doGetRequest(url, data, function(err, txps) { + // Tamper data + txps[0].changeAddress.address = 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5'; + + // Tamper response + clients[0]._doGetRequest = sinon.stub().yields(null, txps); + + // Grab real response + clients[0].getTxProposals({}, function(err, txps) { + should.exist(err); + err.code.should.contain('SERVERCOMPROMISED'); + done(); + }); + }); + }); + }); + }); + }); + }); + + + }); + describe('Transactions Signatures and Rejection', function() { it('Send and broadcast in 1-1 wallet', function(done) { helpers.createAndJoinWallet(clients, 1, 1, function(err, w) { @@ -575,117 +754,5 @@ describe('client API ', function() { }); }); }); - - }); - - describe('Send Transaction Troposals and Locked funds', function() { - it('Should lock and release funds', 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: 'hola 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].rejectTxProposal(x, 'no', function(err, z) { - should.not.exist(err); - z.status.should.equal('rejected'); - clients[0].sendTxProposal(opts, function(err, x) { - should.not.exist(err); - done(); - }); - }); - }); - }); - }); - }); - }); - it('Should keep message and refusal texts', function(done) { - var msg = 'abcdefg'; - helpers.createAndJoinWallet(clients, 2, 3, function(err, w) { - clients[0].createAddress(function(err, x0) { - should.not.exist(err); - blockExplorerMock.setUtxo(x0, 10, 2); - var opts = { - amount: 10000, - toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5', - message: msg, - }; - clients[0].sendTxProposal(opts, function(err, x) { - should.not.exist(err); - clients[1].rejectTxProposal(x, 'xx', function(err, tx1) { - should.not.exist(err); - clients[2].getTxProposals({}, function(err, txs) { - should.not.exist(err); - txs[0].decryptedMessage.should.equal(msg); - _.values(txs[0].actions)[0].comment.should.equal('xx'); - done(); - }); - }); - }); - }); - }); - }); - }); - - - - /* - - - describe('#signTxProposal ', function() { - it.skip('should sign tx proposal', function(done) {}); - - it('should detect fake tx proposal signature', function(done) { - client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.complete11)); - var txp = { - creatorId: '56cb00afd85f4f37fa900ac4e367676f2eb6189a773633eb9f119eb21a22ba44', - toAddress: '2N3fA6wDtnebzywPkGuNK9KkFaEzgbPRRTq', - amount: 100000, - message: 'some message', - proposalSignature: 'dummy', - changeAddress: { - address: '2N3fA6wDtnebzywPkGuNK9KkFaEzgbPRRTq', - path: 'm/2147483647/0/7', - publicKeys: ['03f6a5fe8db51bfbaf26ece22a3e3bc242891a47d3048fc70bc0e8c03a071ad76f'] - }, - }; - client.signTxProposal(txp, function(err) { - err.code.should.equal('SERVERCOMPROMISED'); - err.message.should.contain('fake transaction proposal'); - done(); - }); - }); - - it('should detect fake tx proposal change address', function(done) { - var txp = { - toAddress: '2N3fA6wDtnebzywPkGuNK9KkFaEzgbPRRTq', - amount: 100000, - message: 'some message', - proposalSignature: '3045022100e2d9ef7ed592217ab2256fdcf9627075f35ecdf431dde8c9a9c9422b7b1fb00f02202bc8ce066db4401bdbafb2492c3138debbc69c4c01db50d8c22a227e744c8906', - changeAddress: { - address: '2N3fA6wDtnebzywPkGuNK9KkFaEzgbPRRTq', - path: 'm/2147483647/0/8', - publicKeys: ['03f6a5fe8db51bfbaf26ece22a3e3bc242891a47d3048fc70bc0e8c03a071ad76f'] - }, - }; - client.signTxProposal(txp, function(err) { - err.code.should.equal('SERVERCOMPROMISED'); - err.message.should.contain('fake transaction proposal'); - done(); - }); - }); - }); - */ }); From e6c53e6cb2b8b16b8b3464df03f564e16aba894b Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Fri, 20 Feb 2015 11:56:34 -0300 Subject: [PATCH 3/5] add coverage --- Makefile | 5 +++++ package.json | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..50acf22 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +.PHONY: test +test: + ./node_modules/.bin/mocha +cover: + ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- --reporter spec test diff --git a/package.json b/package.json index 049b877..6f38bd5 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "sinon": "^1.10.3", "memdown": "^1.0.0", "jsdoc": "^3.3.0", - "supertest": "*" + "supertest": "*", + "istanbul": "*" }, "scripts": { "start": "node server.js" From e8dce5adfd667543f1b708f66d6bfc08dfcbd296 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Fri, 20 Feb 2015 12:25:21 -0300 Subject: [PATCH 4/5] update json --- lib/client/api.js | 8 ++- package.json | 98 ++++++++++++++++++----------------- test/integration/clientApi.js | 2 +- 3 files changed, 58 insertions(+), 50 deletions(-) diff --git a/lib/client/api.js b/lib/client/api.js index 32f6d05..1e456e5 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -430,6 +430,11 @@ API.prototype.import = function(str, cb) { }); }; +/** + * + * opts.doNotVerify + * @return {undefined} + */ API.prototype.getTxProposals = function(opts, cb) { var self = this; @@ -444,7 +449,8 @@ API.prototype.getTxProposals = function(opts, cb) { _.each(txps, function(txp) { txp.decryptedMessage = _decryptProposalMessage(txp.message, data.sharedEncryptingKey); - if (!Verifier.checkTxProposal(data, txp)) + if (!opts.doNotVerify + && !Verifier.checkTxProposal(data, txp)) fake = true; }); diff --git a/package.json b/package.json index 6f38bd5..897e04f 100644 --- a/package.json +++ b/package.json @@ -1,50 +1,52 @@ { - "name": "copay-server", - "description": "Copay server", - "author": "isocolsky", - "version": "0.0.1", - "keywords": [ - "bitcoin", - "copay", - "multisig", - "wallet" - ], - "repository": { - "url": "git@github.com:isocolsky/copay-lib.git", - "type": "git" - }, - "bugs": { - "url": "https://github.com/isocolsky/copay-lib/issues" - }, - "dependencies": { - "async": "^0.9.0", - "bitcore": "git+https://github.com/eordano/bitcore.git#7e88167891811163071ae35dc3dbb705ab6ccff8", - "bitcore-explorers": "^0.9.1", - "body-parser": "^1.11.0", - "commander": "^2.6.0", - "express": "^4.10.0", - "inherits": "^2.0.1", - "leveldown": "^0.10.0", - "levelup": "^0.19.0", - "lodash": "^3.2.0", - "morgan": "*", - "npmlog": "^0.1.1", - "preconditions": "^1.0.7", - "qr-image": "*", - "request": "^2.53.0", - "sjcl": "^1.0.2", - "uuid": "*" - }, - "devDependencies": { - "chai": "^1.9.1", - "mocha": "^1.18.2", - "sinon": "^1.10.3", - "memdown": "^1.0.0", - "jsdoc": "^3.3.0", - "supertest": "*", - "istanbul": "*" - }, - "scripts": { - "start": "node server.js" - } + "name": "bitcore-wallet-service", + "description": "A service for Mutisig HD Bitcoin Wallets", + "author": "BitPay Inc", + "version": "0.0.1", + "keywords": [ + "bitcoin", + "copay", + "multisig", + "wallet" + ], + "repository": { + "url": "git@github.com:bitpay/bitcore-wallet-service.git", + "type": "git" + }, + "bugs": { + "url": "https://github.com/bitpay/bitcore-wallet-service/issues" + }, + "dependencies": { + "async": "^0.9.0", + "bitcore": "git+https://github.com/eordano/bitcore.git#7e88167891811163071ae35dc3dbb705ab6ccff8", + "bitcore-explorers": "^0.9.1", + "body-parser": "^1.11.0", + "commander": "^2.6.0", + "express": "^4.10.0", + "inherits": "^2.0.1", + "leveldown": "^0.10.0", + "levelup": "^0.19.0", + "lodash": "^3.2.0", + "morgan": "*", + "npmlog": "^0.1.1", + "preconditions": "^1.0.7", + "qr-image": "*", + "request": "^2.53.0", + "sjcl": "^1.0.2", + "uuid": "*" + }, + "devDependencies": { + "chai": "^1.9.1", + "mocha": "^1.18.2", + "sinon": "^1.10.3", + "memdown": "^1.0.0", + "jsdoc": "^3.3.0", + "supertest": "*", + "istanbul": "*" + }, + "scripts": { + "start": "node server.js", + "coverage": "./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- --reporter spec test", + "test": "./node_modules/.bin/mocha" + } } diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index fcd9f05..411211b 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -414,7 +414,7 @@ describe('client API ', function() { }); - describe('Transaction Troposals Creation and Locked funds', function() { + describe('Transaction Proposals Creation and Locked funds', function() { it('Should lock and release funds', function(done) { helpers.createAndJoinWallet(clients, 2, 2, function(err, w) { clients[0].createAddress(function(err, x0) { From 14a06b675748b6ce5f0f364808511edc8b6e373b Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Fri, 20 Feb 2015 13:59:42 -0300 Subject: [PATCH 5/5] add phony --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 50acf22..fe45937 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: test +.PHONY: test cover test: ./node_modules/.bin/mocha cover: