From 2af599c0add0ffe986cfe0a3b596c0dedbfc066c Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Mon, 15 Dec 2014 19:31:34 +0100 Subject: [PATCH] Refactor account service --- src/js/controller/app/navigation.js | 2 +- src/js/email/account.js | 145 ++++++++-------------- src/js/service/auth.js | 42 +++---- test/unit/email/account-test.js | 183 ++++++++++++++-------------- test/unit/service/auth-test.js | 25 ++-- 5 files changed, 183 insertions(+), 214 deletions(-) diff --git a/src/js/controller/app/navigation.js b/src/js/controller/app/navigation.js index 4a90800..abc2826 100644 --- a/src/js/controller/app/navigation.js +++ b/src/js/controller/app/navigation.js @@ -113,7 +113,7 @@ var NavigationCtrl = function($scope, $location, account, email, outbox, notific message: str.logoutMessage, callback: function(confirm) { if (confirm) { - account.logout(); + account.logout().catch(dialog.error); } } }); diff --git a/src/js/email/account.js b/src/js/email/account.js index b665d36..171ac8e 100644 --- a/src/js/email/account.js +++ b/src/js/email/account.js @@ -41,7 +41,7 @@ Account.prototype.list = function() { /** * Fire up the database, retrieve the available keys for the user and initialize the email data access object */ -Account.prototype.init = function(options, callback) { +Account.prototype.init = function(options) { var self = this; // account information for the email dao @@ -53,68 +53,43 @@ Account.prototype.init = function(options, callback) { // Pre-Flight check: don't even start to initialize stuff if the email address is not valid if (!util.validateEmailAddress(options.emailAddress)) { - return callback(new Error('The user email address is invalid!')); + return new Promise(function() { + throw new Error('The user email address is invalid!'); + }); } - prepareDatabase(); - // Pre-Flight check: initialize and prepare user's local database - function prepareDatabase() { - self._accountStore.init(options.emailAddress, function(err) { - if (err) { - return callback(err); - } + return self._accountStore.init(options.emailAddress).then(function() { + // Migrate the databases if necessary + return self._updateHandler.update().catch(function(err) { + throw new Error('Updating the internal database failed. Please reinstall the app! Reason: ' + err.message); + }); - // Migrate the databases if necessary - self._updateHandler.update(function(err) { - if (err) { - return callback(new Error('Updating the internal database failed. Please reinstall the app! Reason: ' + err.message)); - } + }).then(function() { + // retrieve keypair fom devicestorage/cloud, refresh public key if signup was incomplete before + return self._keychain.getUserKeyPair(options.emailAddress); - prepareKeys(); + }).then(function(keys) { + // this is either a first start on a new device, OR a subsequent start without completing the signup, + // since we can't differenciate those cases here, do a public key refresh because it might be outdated + if (keys && keys.publicKey && !keys.privateKey) { + return self._keychain.refreshKeyForUserId({ + userId: options.emailAddress, + overridePermission: true + }).then(function(publicKey) { + return { + publicKey: publicKey + }; }); + } + // either signup was complete or no pubkey is available, so we're good here. + return keys; - }); - } - - // retrieve keypair fom devicestorage/cloud, refresh public key if signup was incomplete before - function prepareKeys() { - self._keychain.getUserKeyPair(options.emailAddress, function(err, keys) { - if (err) { - return callback(err); - } - - // this is either a first start on a new device, OR a subsequent start without completing the signup, - // since we can't differenciate those cases here, do a public key refresh because it might be outdated - if (keys && keys.publicKey && !keys.privateKey) { - self._keychain.refreshKeyForUserId({ - userId: options.emailAddress, - overridePermission: true - }, function(err, publicKey) { - if (err) { - return callback(err); - } - - initEmailDao({ - publicKey: publicKey - }); - }); - return; - } - - // either signup was complete or no pubkey is available, so we're good here. - initEmailDao(keys); - }); - } - - function initEmailDao(keys) { - self._emailDao.init({ + }).then(function(keys) { + // init the email data access object + return self._emailDao.init({ account: account - }, function(err) { - if (err) { - return callback(err); - } - + }).then(function() { // Handle offline and online gracefully ... arm dom event window.addEventListener('online', self.onConnect.bind(self)); window.addEventListener('offline', self.onDisconnect.bind(self)); @@ -122,9 +97,9 @@ Account.prototype.init = function(options, callback) { // add account object to the accounts array for the ng controllers self._accounts.push(account); - callback(null, keys); + return keys; }); - } + }); }; /** @@ -140,7 +115,7 @@ Account.prototype.isOnline = function() { Account.prototype.onConnect = function(callback) { var self = this; var config = self._appConfig.config; - + callback = callback || self._dialog.error; if (!self.isOnline() || !self._emailDao || !self._emailDao._account) { @@ -148,16 +123,8 @@ Account.prototype.onConnect = function(callback) { return; } - self._auth.getCredentials(function(err, credentials) { - if (err) { - callback(err); - return; - } - - initClients(credentials); - }); - - function initClients(credentials) { + // init imap/smtp clients + self._auth.getCredentials().then(function(credentials) { // add the maximum update batch size for imap folders to the imap configuration credentials.imap.maxUpdateSize = config.imapUpdateBatchSize; @@ -170,16 +137,16 @@ Account.prototype.onConnect = function(callback) { pgpMailer.onError = onConnectionError; // certificate update handling - imapClient.onCert = self._auth.handleCertificateUpdate.bind(self._auth, 'imap', self.onConnect.bind(self)).catch(self._dialog.error); - pgpMailer.onCert = self._auth.handleCertificateUpdate.bind(self._auth, 'smtp', self.onConnect.bind(self)).catch(self._dialog.error); + imapClient.onCert = self._auth.handleCertificateUpdate.bind(self._auth, 'imap', self.onConnect.bind(self), self._dialog.error); + pgpMailer.onCert = self._auth.handleCertificateUpdate.bind(self._auth, 'smtp', self.onConnect.bind(self), self._dialog.error); // connect to clients - self._emailDao.onConnect({ + return self._emailDao.onConnect({ imapClient: imapClient, pgpMailer: pgpMailer, ignoreUploadOnSent: self._emailDao.checkIgnoreUploadOnSent(credentials.imap.host) - }, callback); - } + }); + }).then(callback).catch(callback); function onConnectionError(error) { axe.debug('Connection error. Attempting reconnect in ' + config.reconnectInterval + ' ms. Error: ' + (error.errMsg || error.message) + (error.stack ? ('\n' + error.stack) : '')); @@ -203,7 +170,7 @@ Account.prototype.onConnect = function(callback) { * Event handler that is called when the user agent goes offline. */ Account.prototype.onDisconnect = function() { - this._emailDao.onDisconnect(); + return this._emailDao.onDisconnect(); }; /** @@ -211,28 +178,18 @@ Account.prototype.onDisconnect = function() { */ Account.prototype.logout = function() { var self = this; - // clear app config store - self._auth.logout(function(err) { - if (err) { - self._dialog.error(err); - return; - } - + return self._auth.logout().then(function() { // delete instance of imap-client and pgp-mailer - self._emailDao.onDisconnect(function(err) { - if (err) { - self._dialog.error(err); - return; - } + return self._emailDao.onDisconnect(); - if (typeof window.chrome !== 'undefined' && chrome.runtime && chrome.runtime.reload) { - // reload chrome app - chrome.runtime.reload(); - } else { - // navigate to login - window.location.href = '/'; - } - }); + }).then(function() { + if (typeof window.chrome !== 'undefined' && chrome.runtime && chrome.runtime.reload) { + // reload chrome app + chrome.runtime.reload(); + } else { + // navigate to login + window.location.href = '/'; + } }); }; \ No newline at end of file diff --git a/src/js/service/auth.js b/src/js/service/auth.js index df7d445..ec7f875 100644 --- a/src/js/service/auth.js +++ b/src/js/service/auth.js @@ -310,9 +310,10 @@ Auth.prototype._loadCredentials = function() { /** * Handles certificate updates and errors by notifying the user. * @param {String} component Either imap or smtp + * @param {Function} callback The error handler * @param {[type]} pemEncodedCert The PEM encoded SSL certificate */ -Auth.prototype.handleCertificateUpdate = function(component, onConnect, pemEncodedCert) { +Auth.prototype.handleCertificateUpdate = function(component, onConnect, callback, pemEncodedCert) { var self = this; axe.debug('new ssl certificate received: ' + pemEncodedCert); @@ -321,7 +322,8 @@ Auth.prototype.handleCertificateUpdate = function(component, onConnect, pemEncod // no previous ssl cert, trust on first use self[component].ca = pemEncodedCert; self.credentialsDirty = true; - return self.storeCredentials(); + self.storeCredentials().then(callback).catch(callback); + return; } if (self[component].ca === pemEncodedCert) { @@ -330,26 +332,24 @@ Auth.prototype.handleCertificateUpdate = function(component, onConnect, pemEncod } // previous ssl cert known, does not match: query user and certificate - return new Promise(function() { - throw { - title: str.updateCertificateTitle, - message: str.updateCertificateMessage.replace('{0}', self[component].host), - positiveBtnStr: str.updateCertificatePosBtn, - negativeBtnStr: str.updateCertificateNegBtn, - showNegativeBtn: true, - faqLink: str.certificateFaqLink, - callback: function(granted) { - if (!granted) { - return; - } - - self[component].ca = pemEncodedCert; - self.credentialsDirty = true; - return self.storeCredentials().then(function() { - return onConnect(); - }); + callback({ + title: str.updateCertificateTitle, + message: str.updateCertificateMessage.replace('{0}', self[component].host), + positiveBtnStr: str.updateCertificatePosBtn, + negativeBtnStr: str.updateCertificateNegBtn, + showNegativeBtn: true, + faqLink: str.certificateFaqLink, + callback: function(granted) { + if (!granted) { + return; } - }; + + self[component].ca = pemEncodedCert; + self.credentialsDirty = true; + self.storeCredentials().then(function() { + onConnect(callback); + }).catch(callback); + } }); }; diff --git a/test/unit/email/account-test.js b/test/unit/email/account-test.js index 5e5cd27..ff18481 100644 --- a/test/unit/email/account-test.js +++ b/test/unit/email/account-test.js @@ -58,55 +58,45 @@ describe('Account Service unit test', function() { account.init({ emailAddress: dummyUser.replace('@'), realname: realname - }, onInit); - - function onInit(err, keys) { + }).catch(function onInit(err) { expect(err).to.exist; - expect(keys).to.not.exist; - } + }); }); it('should fail for _accountStore.init', function() { - devicestorageStub.init.yields(new Error('asdf')); + devicestorageStub.init.returns(rejects(new Error('asdf'))); account.init({ emailAddress: dummyUser, realname: realname - }, onInit); - - function onInit(err, keys) { + }).catch(function onInit(err) { expect(err.message).to.match(/asdf/); - expect(keys).to.not.exist; - } + }); }); it('should fail for _updateHandler.update', function() { - updateHandlerStub.update.yields(new Error('asdf')); + devicestorageStub.init.returns(resolves()); + updateHandlerStub.update.returns(rejects(new Error('asdf'))); account.init({ emailAddress: dummyUser, realname: realname - }, onInit); - - function onInit(err, keys) { + }).catch(function onInit(err) { expect(err.message).to.match(/Updating/); - expect(keys).to.not.exist; - } + }); }); it('should fail for _keychain.getUserKeyPair', function() { - updateHandlerStub.update.yields(); - keychainStub.getUserKeyPair.yields(new Error('asdf')); + devicestorageStub.init.returns(resolves()); + updateHandlerStub.update.returns(resolves()); + keychainStub.getUserKeyPair.returns(rejects(new Error('asdf'))); account.init({ emailAddress: dummyUser, realname: realname - }, onInit); - - function onInit(err, keys) { + }).catch(function(err) { expect(err.message).to.match(/asdf/); - expect(keys).to.not.exist; - } + }); }); it('should fail for _keychain.refreshKeyForUserId', function() { @@ -114,19 +104,17 @@ describe('Account Service unit test', function() { publicKey: 'publicKey' }; - updateHandlerStub.update.yields(); - keychainStub.getUserKeyPair.yields(null, storedKeys); - keychainStub.refreshKeyForUserId.yields(new Error('asdf')); + devicestorageStub.init.returns(resolves()); + updateHandlerStub.update.returns(resolves()); + keychainStub.getUserKeyPair.returns(resolves(storedKeys)); + keychainStub.refreshKeyForUserId.returns(rejects(new Error('asdf'))); account.init({ emailAddress: dummyUser, realname: realname - }, onInit); - - function onInit(err, keys) { + }).catch(function(err) { expect(err.message).to.match(/asdf/); - expect(keys).to.not.exist; - } + }); }); it('should fail for _emailDao.init after _keychain.refreshKeyForUserId', function() { @@ -134,20 +122,18 @@ describe('Account Service unit test', function() { publicKey: 'publicKey' }; - updateHandlerStub.update.yields(); - keychainStub.getUserKeyPair.yields(null, storedKeys); - keychainStub.refreshKeyForUserId.yields(null, storedKeys); - emailStub.init.yields(new Error('asdf')); + devicestorageStub.init.returns(resolves()); + updateHandlerStub.update.returns(resolves()); + keychainStub.getUserKeyPair.returns(resolves(storedKeys)); + keychainStub.refreshKeyForUserId.returns(resolves(storedKeys)); + emailStub.init.returns(rejects(new Error('asdf'))); account.init({ emailAddress: dummyUser, realname: realname - }, onInit); - - function onInit(err, keys) { + }).catch(function(err) { expect(err.message).to.match(/asdf/); - expect(keys).to.not.exist; - } + }); }); it('should fail for _emailDao.init', function() { @@ -156,19 +142,17 @@ describe('Account Service unit test', function() { privateKey: 'privateKey' }; - updateHandlerStub.update.yields(); - keychainStub.getUserKeyPair.yields(null, storedKeys); - emailStub.init.yields(new Error('asdf')); + devicestorageStub.init.returns(resolves()); + updateHandlerStub.update.returns(resolves()); + keychainStub.getUserKeyPair.returns(resolves(storedKeys)); + emailStub.init.returns(rejects(new Error('asdf'))); account.init({ emailAddress: dummyUser, realname: realname - }, onInit); - - function onInit(err, keys) { + }).catch(function(err) { expect(err.message).to.match(/asdf/); - expect(keys).to.not.exist; - } + }); }); it('should work after _keychain.refreshKeyForUserId', function() { @@ -176,20 +160,20 @@ describe('Account Service unit test', function() { publicKey: 'publicKey' }; - updateHandlerStub.update.yields(); - keychainStub.getUserKeyPair.yields(null, storedKeys); - keychainStub.refreshKeyForUserId.yields(null, 'publicKey'); - emailStub.init.yields(); + devicestorageStub.init.returns(resolves()); + updateHandlerStub.update.returns(resolves()); + keychainStub.getUserKeyPair.returns(resolves(storedKeys)); + keychainStub.refreshKeyForUserId.returns(resolves('publicKey')); + emailStub.init.returns(resolves()); account.init({ emailAddress: dummyUser, realname: realname - }, onInit); - - function onInit(err, keys) { - expect(err).to.not.exist; + }, function onInit(keys) { expect(keys).to.deep.equal(storedKeys); - } + expect(keychainStub.refreshKeyForUserId.calledOnce).to.be.true; + expect(emailStub.init.calledOnce).to.be.true; + }); }); it('should work', function() { @@ -198,19 +182,20 @@ describe('Account Service unit test', function() { privateKey: 'privateKey' }; - updateHandlerStub.update.yields(); - keychainStub.getUserKeyPair.yields(null, storedKeys); - emailStub.init.yields(); + devicestorageStub.init.returns(resolves()); + updateHandlerStub.update.returns(resolves()); + keychainStub.getUserKeyPair.returns(resolves(storedKeys)); + emailStub.init.returns(resolves()); account.init({ emailAddress: dummyUser, realname: realname - }, onInit); - - function onInit(err, keys) { - expect(err).to.not.exist; + }, function onInit(keys) { expect(keys).to.equal(storedKeys); - } + expect(keychainStub.refreshKeyForUserId.called).to.be.false; + expect(emailStub.init.calledOnce).to.be.true; + expect(account._accounts.length).to.equal(1); + }); }); }); @@ -227,48 +212,66 @@ describe('Account Service unit test', function() { account.isOnline.restore(); }); - it('should fail due to _auth.getCredentials', function() { - authStub.getCredentials.yields(new Error('asdf')); + it('should fail due to _auth.getCredentials', function(done) { + authStub.getCredentials.returns(rejects(new Error('asdf'))); + + dialogStub.error = function(err) { + expect(err.message).to.match(/asdf/); + done(); + }; account.onConnect(); - - expect(dialogStub.error.calledOnce).to.be.true; }); - it('should work', function() { - authStub.getCredentials.yields(null, credentials); - emailStub.onConnect.yields(); + it('should fail due to _auth.getCredentials', function(done) { + authStub.getCredentials.returns(rejects(new Error('asdf'))); - account.onConnect(); + account.onConnect(function(err) { + expect(err.message).to.match(/asdf/); + expect(dialogStub.error.called).to.be.false; + done(); + }); + }); - expect(emailStub.onConnect.calledOnce).to.be.true; - expect(dialogStub.error.calledOnce).to.be.true; + it('should work', function(done) { + authStub.getCredentials.returns(resolves(credentials)); + authStub.handleCertificateUpdate.returns(resolves()); + emailStub.onConnect.returns(resolves()); + + account.onConnect(function(err) { + expect(err).to.not.exist; + expect(dialogStub.error.called).to.be.false; + expect(emailStub.onConnect.calledOnce).to.be.true; + done(); + }); }); }); describe('onDisconnect', function() { - it('should work', function() { - account.onDisconnect(); - expect(emailStub.onDisconnect.calledOnce).to.be.true; + it('should work', function(done) { + emailStub.onDisconnect.returns(resolves()); + account.onDisconnect().then(done); }); }); describe('logout', function() { - it('should fail due to _auth.logout', function() { - authStub.logout.yields(new Error()); + it('should fail due to _auth.logout', function(done) { + authStub.logout.returns(rejects(new Error('asdf'))); - account.logout(); - - expect(dialogStub.error.calledOnce).to.be.true; + account.logout().catch(function(err) { + expect(err.message).to.match(/asdf/); + done(); + }); }); - it('should fail due to _emailDao.onDisconnect', function() { - authStub.logout.yields(); - emailStub.onDisconnect.yields(new Error()); + it('should fail due to _emailDao.onDisconnect', function(done) { + authStub.logout.returns(resolves()); + emailStub.onDisconnect.returns(rejects(new Error('asdf'))); - account.logout(); - - expect(dialogStub.error.calledOnce).to.be.true; + account.logout().catch(function(err) { + expect(err.message).to.match(/asdf/); + done(); + }); }); }); diff --git a/test/unit/service/auth-test.js b/test/unit/service/auth-test.js index 17a81b1..b7f70f3 100644 --- a/test/unit/service/auth-test.js +++ b/test/unit/service/auth-test.js @@ -311,7 +311,8 @@ describe('Auth unit tests', function() { expect(storeCredentialsStub.callCount).to.equal(1); done(); } - auth.handleCertificateUpdate('imap', onConnectDummy, callback, dummyCert).then(callback); + + auth.handleCertificateUpdate('imap', onConnectDummy, callback, dummyCert); }); it('should work for stored cert', function() { @@ -320,7 +321,10 @@ describe('Auth unit tests', function() { }; storeCredentialsStub.returns(resolves()); - auth.handleCertificateUpdate('imap', onConnectDummy, dummyCert); + function callback() {} + + auth.handleCertificateUpdate('imap', onConnectDummy, callback, dummyCert); + expect(storeCredentialsStub.callCount).to.equal(0); }); @@ -337,7 +341,8 @@ describe('Auth unit tests', function() { expect(storeCredentialsStub.callCount).to.equal(0); done(); } - auth.handleCertificateUpdate('imap', onConnectDummy, dummyCert).catch(callback); + + auth.handleCertificateUpdate('imap', onConnectDummy, callback, dummyCert); }); it('should work for updated cert', function(done) { @@ -351,14 +356,18 @@ describe('Auth unit tests', function() { expect(err).to.exist; expect(err.message).to.exist; expect(storeCredentialsStub.callCount).to.equal(0); - err.callback(true).then(function() { - expect(storeCredentialsStub.callCount).to.equal(1); - done(); - }); + err.callback(true); + } else { + expect(storeCredentialsStub.callCount).to.equal(1); + done(); } } - auth.handleCertificateUpdate('imap', onConnectDummy, dummyCert).catch(callback); + function onConnect(cb) { + cb(); + } + + auth.handleCertificateUpdate('imap', onConnect, callback, dummyCert); }); });