From 8166fa5c8e9c460b552c40c4ea6e009cd3c67cb3 Mon Sep 17 00:00:00 2001 From: "Ryan X. Charles" Date: Tue, 15 Apr 2014 18:06:42 -0300 Subject: [PATCH 1/5] change default network to "Base" ...since it won't be used for the command-line (yet). --- API.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/API.js b/API.js index dd6d2fe19..cf6e68bfe 100644 --- a/API.js +++ b/API.js @@ -12,7 +12,7 @@ API.prototype._init = function(opts) { var Wallet = require('soop').load('./js/models/core/Wallet', { Storage: opts.Storage || require('./test/mocks/FakeStorage'), - Network: opts.Network || require('./js/models/network/WebRTC'), + Network: opts.Network || require('./js/models/network/Base'), Blockchain: opts.Blockchain || require('./js/models/blockchain/Insight') }); From 08f918a9a7a5a3ab97fd12cabfa974a333f3fa88 Mon Sep 17 00:00:00 2001 From: "Ryan X. Charles" Date: Wed, 16 Apr 2014 15:26:04 -0300 Subject: [PATCH 2/5] add File storage class and tests --- bin/copay | 5 +- js/models/storage/File.js | 87 ++++++++++++++++++++++ package.json | 3 +- test/test.storage.File.js | 147 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 238 insertions(+), 4 deletions(-) create mode 100644 js/models/storage/File.js create mode 100644 test/test.storage.File.js diff --git a/bin/copay b/bin/copay index 2721da0e2..9890f66cd 100755 --- a/bin/copay +++ b/bin/copay @@ -18,11 +18,10 @@ var main = function() { var api = new API(commander); var args = commander.args; + var command = args[0]; + var commandArgs = args.slice(1); try { - var command = args[0]; - var commandArgs = args.slice(1); - if (command[0] == '_' || typeof api[command] != 'function') throw new Error('invalid command'); diff --git a/js/models/storage/File.js b/js/models/storage/File.js new file mode 100644 index 000000000..1f659f8fc --- /dev/null +++ b/js/models/storage/File.js @@ -0,0 +1,87 @@ +'use strict'; +var imports = require('soop').imports(); +var fs = imports.fs || require('fs'); + +function Storage(opts) { + opts = opts || {}; + + this.data = {}; + this.filename = opts.filename; +} + +Storage.prototype.load = function(callback) { + if (!this.filename) + throw new Error('No filename'); + + fs.readFile(this.filename, function(err, data) { + if (err) return callback(err); + + try { + this.data = JSON.parse(data); + } catch (err) { + return callback(err); + } + + return callback(null); + }); +}; + +Storage.prototype.save = function(callback) { + var data = JSON.stringify(this.data); + + //TODO: update to use a queue to ensure that saves are made sequentially + fs.writeFile(this.filename, data, function(err) { + return callback(err); + }); +}; + +Storage.prototype._read = function(k) { + return this.data[k]; +}; + +Storage.prototype._write = function(k, v, callback) { + this.data[k] = v; + this.save(callback); +}; + +// get value by key +Storage.prototype.getGlobal = function(k) { + return this.data[k]; +}; + +// set value for key +Storage.prototype.setGlobal = function(k, v, callback) { + this._write(k, v, callback); +}; + +// remove value for key +Storage.prototype.removeGlobal = function(k, callback) { + delete this.data[k]; + this.save(callback); +}; + +Storage.prototype._key = function(walletId, k) { + return walletId + '::' + k; +}; +// get value by key +Storage.prototype.get = function(walletId, k) { + return this.getGlobal(this._key(walletId, k)); +}; + +// set value for key +Storage.prototype.set = function(walletId, k, v, callback) { + this.setGlobal(this._key(walletId,k), v, callback); +}; + +// remove value for key +Storage.prototype.remove = function(walletId, k, callback) { + this.removeGlobal(this._key(walletId, k), callback); +}; + +// remove all values +Storage.prototype.clearAll = function(callback) { + this.data = {}; + this.save(callback); +}; + +module.exports = require('soop')(Storage); diff --git a/package.json b/package.json index c16e0a9a7..7e0cbdd62 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "uglifyify": "~1.2.3", "soop": "~0.1.5", "bitcore": "git://github.com/maraoz/bitcore.git#5e636f6b9c7f8e629b1a502025556e886c3b75e1", - "chai": "~1.9.1" + "chai": "~1.9.1", + "sinon": "~1.9.1" } } diff --git a/test/test.storage.File.js b/test/test.storage.File.js new file mode 100644 index 000000000..d3d47ca96 --- /dev/null +++ b/test/test.storage.File.js @@ -0,0 +1,147 @@ +'use strict'; + +var chai = chai || require('chai'); +var should = chai.should(); +var Storage = Storage || require('../js/models/storage/File.js'); +var sinon = sinon || require('sinon'); + +describe('Storage/File', function() { + it('should exist', function() { + should.exist(Storage); + }); + + describe('#load', function(done) { + it('should call fs.readFile', function(done) { + var fs = {} + fs.readFile = function(filename, callback) { + filename.should.equal('myfilename'); + callback(); + }; + var Storage = require('soop').load('../js/models/storage/File.js', {fs: fs}); + var storage = new Storage({filename: 'myfilename', password: 'password'}); + storage.load(function(err) { + done(); + }); + }); + }); + + describe('#save', function(done) { + it('should call fs.writeFile', function(done) { + var fs = {} + fs.writeFile = function(filename, data, callback) { + filename.should.equal('myfilename'); + callback(); + }; + var Storage = require('soop').load('../js/models/storage/File.js', {fs: fs}); + var storage = new Storage({filename: 'myfilename', password: 'password'}); + storage.save(function(err) { + done(); + }); + }); + }); + + describe('#_read', function() { + it('should return the value of a key', function() { + var storage = new Storage(); + storage.data = {'test':'data'}; + storage._read('test').should.equal('data'); + }); + }); + + describe('#_write', function() { + it('should save the value of a key and then run save', function(done) { + var storage = new Storage(); + storage.save = function(callback) { + storage.data['key'].should.equal('value'); + callback(); + }; + storage._write('key', 'value', function() { + done(); + }); + }); + }); + + describe('#getGlobal', function() { + it('should store a global key', function(done) { + var storage = new Storage(); + storage.save = function(callback) { + storage.data['key'].should.equal('value'); + callback(); + }; + storage.setGlobal('key', 'value', function() { + done(); + }); + }); + }); + + describe('#setGlobal', function() { + it('should store a global key', function(done) { + var storage = new Storage(); + storage.save = function(callback) { + storage.data['key'].should.equal('value'); + callback(); + }; + storage.setGlobal('key', 'value', function() { + done(); + }); + }); + }); + + describe('#removeGlobal', function() { + it('should remove a global key', function(done) { + var storage = new Storage(); + storage.data.key = 'value'; + storage.save = function(callback) { + should.not.exist(storage.data['key']); + callback(); + }; + storage.removeGlobal('key', function() { + done(); + }); + }); + }); + + describe('#_key', function() { + it('should merge the wallet id and item key', function() { + var storage = new Storage(); + storage._key('wallet', 'key').should.equal('wallet::key'); + }); + }); + + describe('#get', function() { + it('should call getGlobal with the correct key', function() { + var storage = new Storage(); + storage.getGlobal = sinon.spy(); + storage.get('wallet', 'key'); + storage.getGlobal.calledOnce.should.equal(true); + storage.getGlobal.calledWith('wallet::key').should.equal(true); + }); + }); + + describe('#set', function() { + it('should call setGlobal with the correct key', function() { + var storage = new Storage(); + storage.setGlobal = sinon.spy(); + storage.set('wallet', 'key'); + storage.setGlobal.calledOnce.should.equal(true); + storage.setGlobal.calledWith('wallet::key').should.equal(true); + }); + }); + + describe('#remove', function() { + it('should call removeGlobal with the correct key', function() { + var storage = new Storage(); + storage.removeGlobal = sinon.spy(); + storage.remove('wallet', 'key'); + storage.removeGlobal.calledOnce.should.equal(true); + storage.removeGlobal.calledWith('wallet::key').should.equal(true); + }); + }); + + describe('#clearAll', function() { + it('should set data to {}', function() { + + }); + }); + +}); From b442e110e4c51b39e804c35f547c5fd3cc3d0b54 Mon Sep 17 00:00:00 2001 From: "Ryan X. Charles" Date: Wed, 16 Apr 2014 16:52:42 -0300 Subject: [PATCH 3/5] make tests work by faking storage --- API.js | 3 +++ test/test.API.js | 20 +++++++++++--------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/API.js b/API.js index cf6e68bfe..5cf8d0841 100644 --- a/API.js +++ b/API.js @@ -20,6 +20,9 @@ API.prototype._init = function(opts) { wallet: { requiredCopayers: opts.requiredCopayers || 3, totalCopayers: opts.totalCopayers || 5, + }, + storage: { + filename: 'copaywallet.json' } }; diff --git a/test/test.API.js b/test/test.API.js index 75161997e..a3046d20e 100644 --- a/test/test.API.js +++ b/test/test.API.js @@ -4,11 +4,13 @@ var chai = chai || require('chai'); var should = chai.should(); var copay = copay || require('../copay'); var API = API || copay.API; +var Storage = Storage || require('../test/mocks/FakeStorage'); describe('API', function() { it('should have a command called "echo"', function() { - var api = new API(); + + var api = new API({Storage: Storage}); should.exist(api.echo); }); @@ -22,7 +24,7 @@ describe('API', function() { }) it('should throw an error for all commands when called with wrong number of arguments', function() { - var api = new API(); + var api = new API({Storage: Storage}); for (var i in API.prototype) { var f = API.prototype[i]; if (i[0] != '_' && typeof f == 'function') { @@ -49,7 +51,7 @@ describe('API', function() { describe('#echo', function() { it('should echo a string', function(done) { - var api = new API(); + var api = new API({Storage: Storage}); var str = 'mystr'; api.echo(str, function(err, result) { result.should.equal(str); @@ -60,7 +62,7 @@ describe('API', function() { describe('#echoNumber', function() { it('should echo a number', function(done) { - var api = new API(); + var api = new API({Storage: Storage}); var num = 500; api.echoNumber(num, function(err, result) { result.should.equal(num); @@ -72,7 +74,7 @@ describe('API', function() { describe('#echoObject', function() { it('should echo an object', function(done) { - var api = new API(); + var api = new API({Storage: Storage}); var obj = {test:'test'}; api.echoObject(obj, function(err, result) { result.test.should.equal(obj.test); @@ -84,7 +86,7 @@ describe('API', function() { describe('#getArgTypes', function() { it('should get the argTypes of echo', function(done) { - var api = new API(); + var api = new API({Storage: Storage}); api.getArgTypes('echo', function(err, result) { result[0][1].should.equal('string'); done(); @@ -94,7 +96,7 @@ describe('API', function() { describe('#getCommands', function() { it('should get all the commands', function(done) { - var api = new API(); + var api = new API({Storage: Storage}); var n = 0; for (var i in api) @@ -110,7 +112,7 @@ describe('API', function() { describe('#getPublicKeyRingId', function() { it('should get a public key ring ID', function(done) { - var api = new API(); + var api = new API({Storage: Storage}); api.getPublicKeyRingId(function(err, result) { result.length.should.be.greaterThan(5); done(); @@ -120,7 +122,7 @@ describe('API', function() { describe('#help', function() { it('should call _cmd_getCommands', function(done) { - var api = new API(); + var api = new API({Storage: Storage}); api._cmd_getCommands = function(callback) { (typeof arguments[0]).should.equal('function'); callback(null, ['item']); From 5f8deb7d0ba343f849c836b37764a696af17625d Mon Sep 17 00:00:00 2001 From: "Ryan X. Charles" Date: Wed, 16 Apr 2014 17:37:32 -0300 Subject: [PATCH 4/5] update File to write wallets to different files the walletId is the filename --- js/models/storage/File.js | 49 ++++++++++++++++++++++++--------------- test/test.storage.File.js | 45 +++++++++++++++++------------------ 2 files changed, 51 insertions(+), 43 deletions(-) diff --git a/js/models/storage/File.js b/js/models/storage/File.js index 1f659f8fc..9b3fa5f34 100644 --- a/js/models/storage/File.js +++ b/js/models/storage/File.js @@ -6,47 +6,54 @@ function Storage(opts) { opts = opts || {}; this.data = {}; - this.filename = opts.filename; } -Storage.prototype.load = function(callback) { - if (!this.filename) - throw new Error('No filename'); - - fs.readFile(this.filename, function(err, data) { +Storage.prototype.load = function(walletId, callback) { + fs.readFile(walletId, function(err, data) { if (err) return callback(err); try { - this.data = JSON.parse(data); + this.data[walletId] = JSON.parse(data); } catch (err) { - return callback(err); + if (callback) + return callback(err); } - return callback(null); + if (callback) + return callback(null); }); }; -Storage.prototype.save = function(callback) { - var data = JSON.stringify(this.data); +Storage.prototype.save = function(walletId, callback) { + var data = JSON.stringify(this.data[walletId]); //TODO: update to use a queue to ensure that saves are made sequentially - fs.writeFile(this.filename, data, function(err) { - return callback(err); + fs.writeFile(walletId, data, function(err) { + if (callback) + return callback(err); }); }; Storage.prototype._read = function(k) { - return this.data[k]; + var split = k.split('::'); + var walletId = split[0]; + var key = split[1]; + return this.data[walletId][key]; }; Storage.prototype._write = function(k, v, callback) { - this.data[k] = v; - this.save(callback); + var split = k.split('::'); + var walletId = split[0]; + var key = split[1]; + if (!this.data[walletId]) + this.data[walletId] = {}; + this.data[walletId][key] = v; + this.save(walletId, callback); }; // get value by key Storage.prototype.getGlobal = function(k) { - return this.data[k]; + return this._read(k); }; // set value for key @@ -56,13 +63,17 @@ Storage.prototype.setGlobal = function(k, v, callback) { // remove value for key Storage.prototype.removeGlobal = function(k, callback) { - delete this.data[k]; - this.save(callback); + var split = k.split('::'); + var walletId = split[0]; + var key = split[1]; + delete this.data[walletId][key]; + this.save(walletId, callback); }; Storage.prototype._key = function(walletId, k) { return walletId + '::' + k; }; + // get value by key Storage.prototype.get = function(walletId, k) { return this.getGlobal(this._key(walletId, k)); diff --git a/test/test.storage.File.js b/test/test.storage.File.js index d3d47ca96..b54a34215 100644 --- a/test/test.storage.File.js +++ b/test/test.storage.File.js @@ -18,8 +18,8 @@ describe('Storage/File', function() { callback(); }; var Storage = require('soop').load('../js/models/storage/File.js', {fs: fs}); - var storage = new Storage({filename: 'myfilename', password: 'password'}); - storage.load(function(err) { + var storage = new Storage({password: 'password'}); + storage.load('myfilename', function(err) { done(); }); }); @@ -33,8 +33,8 @@ describe('Storage/File', function() { callback(); }; var Storage = require('soop').load('../js/models/storage/File.js', {fs: fs}); - var storage = new Storage({filename: 'myfilename', password: 'password'}); - storage.save(function(err) { + var storage = new Storage({password: 'password'}); + storage.save('myfilename', function(err) { done(); }); }); @@ -43,45 +43,42 @@ describe('Storage/File', function() { describe('#_read', function() { it('should return the value of a key', function() { var storage = new Storage(); - storage.data = {'test':'data'}; - storage._read('test').should.equal('data'); + storage.data = {'walletId':{'test':'data'}}; + storage._read('walletId::test').should.equal('data'); }); }); describe('#_write', function() { it('should save the value of a key and then run save', function(done) { var storage = new Storage(); - storage.save = function(callback) { - storage.data['key'].should.equal('value'); + storage.save = function(walletId, callback) { + storage.data[walletId]['key'].should.equal('value'); callback(); }; - storage._write('key', 'value', function() { + storage._write('walletId::key', 'value', function() { done(); }); }); }); describe('#getGlobal', function() { - it('should store a global key', function(done) { + it('should call storage._read', function() { var storage = new Storage(); - storage.save = function(callback) { - storage.data['key'].should.equal('value'); - callback(); - }; - storage.setGlobal('key', 'value', function() { - done(); - }); + storage.data = {'walletId':{'test':'test'}}; + storage._read = sinon.spy(); + storage.getGlobal('walletId::test'); + storage._read.calledOnce.should.equal(true); }); }); describe('#setGlobal', function() { it('should store a global key', function(done) { var storage = new Storage(); - storage.save = function(callback) { - storage.data['key'].should.equal('value'); + storage.save = function(walletId, callback) { + storage.data[walletId]['key'].should.equal('value'); callback(); }; - storage.setGlobal('key', 'value', function() { + storage.setGlobal('walletId::key', 'value', function() { done(); }); }); @@ -90,12 +87,12 @@ describe('Storage/File', function() { describe('#removeGlobal', function() { it('should remove a global key', function(done) { var storage = new Storage(); - storage.data.key = 'value'; - storage.save = function(callback) { - should.not.exist(storage.data['key']); + storage.data = {'walletId':{'key':'value'}}; + storage.save = function(walletId, callback) { + should.not.exist(storage.data[walletId]['key']); callback(); }; - storage.removeGlobal('key', function() { + storage.removeGlobal('walletId::key', function() { done(); }); }); From 96a6203bb0ada1a9895449756c4987b0ff7c8872 Mon Sep 17 00:00:00 2001 From: "Ryan X. Charles" Date: Wed, 16 Apr 2014 21:02:53 -0300 Subject: [PATCH 5/5] make my code work with the latest interface changes ...to Wallet and WalletFactory --- API.js | 23 +++++------------------ js/models/core/WalletFactory.js | 22 ++-------------------- js/models/storage/Base.js | 3 +++ js/models/storage/File.js | 4 ++++ js/models/storage/LocalPlain.js | 15 +++++++++++++++ test/mocks/FakeStorage.js | 4 ++++ test/test.API.js | 8 ++++---- 7 files changed, 37 insertions(+), 42 deletions(-) diff --git a/API.js b/API.js index 5cf8d0841..76f30a29e 100644 --- a/API.js +++ b/API.js @@ -10,26 +10,13 @@ API.prototype._init = function(opts) { opts = opts || {}; self.opts = opts; - var Wallet = require('soop').load('./js/models/core/Wallet', { + var WalletFactory = require('soop').load('./js/models/core/WalletFactory', { Storage: opts.Storage || require('./test/mocks/FakeStorage'), Network: opts.Network || require('./js/models/network/Base'), Blockchain: opts.Blockchain || require('./js/models/blockchain/Insight') }); - - var config = { - wallet: { - requiredCopayers: opts.requiredCopayers || 3, - totalCopayers: opts.totalCopayers || 5, - }, - storage: { - filename: 'copaywallet.json' - } - }; - var walletConfig = opts.walletConfig || config; - var walletOpts = opts.walletOpts || {}; - - self.wallet = self.opts.wallet || Wallet.factory.create(walletConfig, walletOpts); + this.walletFactory = new WalletFactory(opts); }; API._coerceArgTypes = function(args, argTypes) { @@ -182,13 +169,13 @@ API.prototype.getCommands = decorate('getCommands', [ ['callback', 'function'] ]); -API.prototype._cmd_getPublicKeyRingId = function(callback) { +API.prototype._cmd_getWalletIds = function(callback) { var self = this; - return callback(null, self.wallet.publicKeyRing.walletId); + return callback(null, self.walletFactory.getWalletIds()); }; -API.prototype.getPublicKeyRingId = decorate('getPublicKeyRingId', [ +API.prototype.getWalletIds = decorate('getWalletIds', [ ['callback', 'function'] ]); diff --git a/js/models/core/WalletFactory.js b/js/models/core/WalletFactory.js index baef0b27c..3bcd14070 100644 --- a/js/models/core/WalletFactory.js +++ b/js/models/core/WalletFactory.js @@ -110,7 +110,6 @@ WalletFactory.prototype.create = function(opts) { opts.totalCopayers = totalCopayers; var w = new Wallet(opts); w.store(); - this.addWalletId(w.id); return w; }; @@ -156,28 +155,11 @@ WalletFactory.prototype.openRemote = function(peedId) { }; WalletFactory.prototype.getWalletIds = function() { - var ids = this.storage.getGlobal('walletIds'); - return ids || []; + return this.storage.getWalletIds(); } -WalletFactory.prototype._delWalletId = function(walletId) { - var ids = this.getWalletIds(); - var index = ids.indexOf(walletId); - if (index === -1) return; - ids.splice(index, 1); // removes walletId - this.storage.setGlobal('walletIds', ids); -}; - WalletFactory.prototype.remove = function(walletId) { - WalletFactory._delWalletId(walletId); - // TODO remove wallet contents, not only the id (Wallet.remove?) -}; - -WalletFactory.prototype.addWalletId = function(walletId) { - var ids = this.getWalletIds(); - if (ids.indexOf(walletId) !== -1) return; - ids.push(walletId); - this.storage.setGlobal('walletIds', ids); + // TODO remove wallet contents }; diff --git a/js/models/storage/Base.js b/js/models/storage/Base.js index 8f61f4dfe..29ca2564c 100644 --- a/js/models/storage/Base.js +++ b/js/models/storage/Base.js @@ -17,6 +17,9 @@ Storage.prototype.set = function(walletId,v) { Storage.prototype.remove = function(walletId, k) { }; +Storage.prototype.getWalletIds = function() { +}; + // remove all values Storage.prototype.clearAll = function() { }; diff --git a/js/models/storage/File.js b/js/models/storage/File.js index 9b3fa5f34..2c07b7010 100644 --- a/js/models/storage/File.js +++ b/js/models/storage/File.js @@ -89,6 +89,10 @@ Storage.prototype.remove = function(walletId, k, callback) { this.removeGlobal(this._key(walletId, k), callback); }; +Storage.prototype.getWalletIds = function() { + return []; +}; + // remove all values Storage.prototype.clearAll = function(callback) { this.data = {}; diff --git a/js/models/storage/LocalPlain.js b/js/models/storage/LocalPlain.js index 1bda410f2..d32234899 100644 --- a/js/models/storage/LocalPlain.js +++ b/js/models/storage/LocalPlain.js @@ -52,6 +52,21 @@ Storage.prototype.remove = function(walletId, k) { this.removeGlobal(this._key(walletId,k)); }; +Storage.prototype.getWalletIds = function() { + var walletIds = []; + + for (var i = 0; i < localStorage.length; i++) { + var key = localStorage.key(i); + var split = key.split('::'); + if (split.length == 2) { + var walletId = split[0]; + walletIds.push(walletId); + } + } + + return walletIds; +}; + // remove all values Storage.prototype.clearAll = function() { localStorage.clear(); diff --git a/test/mocks/FakeStorage.js b/test/mocks/FakeStorage.js index ef50df677..5819905e5 100644 --- a/test/mocks/FakeStorage.js +++ b/test/mocks/FakeStorage.js @@ -23,4 +23,8 @@ FakeStorage.prototype.clear = function() { delete this['storage']; } +FakeStorage.prototype.getWalletIds = function() { + return []; +}; + module.exports = require('soop')(FakeStorage); diff --git a/test/test.API.js b/test/test.API.js index a3046d20e..96795f75a 100644 --- a/test/test.API.js +++ b/test/test.API.js @@ -110,11 +110,11 @@ describe('API', function() { }); }); - describe('#getPublicKeyRingId', function() { - it('should get a public key ring ID', function(done) { + describe('#getWalletIds', function() { + it('should get the wallet ids', function(done) { var api = new API({Storage: Storage}); - api.getPublicKeyRingId(function(err, result) { - result.length.should.be.greaterThan(5); + api.getWalletIds(function(err, result) { + result.length.should.be.greaterThan(-1); done(); }); });