Refactor account service

This commit is contained in:
Tankred Hase 2014-12-15 19:31:34 +01:00
parent 993ca8eac7
commit 2af599c0ad
5 changed files with 183 additions and 214 deletions

View File

@ -113,7 +113,7 @@ var NavigationCtrl = function($scope, $location, account, email, outbox, notific
message: str.logoutMessage, message: str.logoutMessage,
callback: function(confirm) { callback: function(confirm) {
if (confirm) { if (confirm) {
account.logout(); account.logout().catch(dialog.error);
} }
} }
}); });

View File

@ -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 * 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; var self = this;
// account information for the email dao // 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 // Pre-Flight check: don't even start to initialize stuff if the email address is not valid
if (!util.validateEmailAddress(options.emailAddress)) { 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 // Pre-Flight check: initialize and prepare user's local database
function prepareDatabase() { return self._accountStore.init(options.emailAddress).then(function() {
self._accountStore.init(options.emailAddress, function(err) { // Migrate the databases if necessary
if (err) { return self._updateHandler.update().catch(function(err) {
return callback(err); throw new Error('Updating the internal database failed. Please reinstall the app! Reason: ' + err.message);
} });
// Migrate the databases if necessary }).then(function() {
self._updateHandler.update(function(err) { // retrieve keypair fom devicestorage/cloud, refresh public key if signup was incomplete before
if (err) { return self._keychain.getUserKeyPair(options.emailAddress);
return callback(new Error('Updating the internal database failed. Please reinstall the app! Reason: ' + err.message));
}
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;
}); }).then(function(keys) {
} // init the email data access object
return self._emailDao.init({
// 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({
account: account account: account
}, function(err) { }).then(function() {
if (err) {
return callback(err);
}
// Handle offline and online gracefully ... arm dom event // Handle offline and online gracefully ... arm dom event
window.addEventListener('online', self.onConnect.bind(self)); window.addEventListener('online', self.onConnect.bind(self));
window.addEventListener('offline', self.onDisconnect.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 // add account object to the accounts array for the ng controllers
self._accounts.push(account); self._accounts.push(account);
callback(null, keys); return keys;
}); });
} });
}; };
/** /**
@ -140,7 +115,7 @@ Account.prototype.isOnline = function() {
Account.prototype.onConnect = function(callback) { Account.prototype.onConnect = function(callback) {
var self = this; var self = this;
var config = self._appConfig.config; var config = self._appConfig.config;
callback = callback || self._dialog.error; callback = callback || self._dialog.error;
if (!self.isOnline() || !self._emailDao || !self._emailDao._account) { if (!self.isOnline() || !self._emailDao || !self._emailDao._account) {
@ -148,16 +123,8 @@ Account.prototype.onConnect = function(callback) {
return; return;
} }
self._auth.getCredentials(function(err, credentials) { // init imap/smtp clients
if (err) { self._auth.getCredentials().then(function(credentials) {
callback(err);
return;
}
initClients(credentials);
});
function initClients(credentials) {
// add the maximum update batch size for imap folders to the imap configuration // add the maximum update batch size for imap folders to the imap configuration
credentials.imap.maxUpdateSize = config.imapUpdateBatchSize; credentials.imap.maxUpdateSize = config.imapUpdateBatchSize;
@ -170,16 +137,16 @@ Account.prototype.onConnect = function(callback) {
pgpMailer.onError = onConnectionError; pgpMailer.onError = onConnectionError;
// certificate update handling // certificate update handling
imapClient.onCert = self._auth.handleCertificateUpdate.bind(self._auth, 'imap', 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)).catch(self._dialog.error); pgpMailer.onCert = self._auth.handleCertificateUpdate.bind(self._auth, 'smtp', self.onConnect.bind(self), self._dialog.error);
// connect to clients // connect to clients
self._emailDao.onConnect({ return self._emailDao.onConnect({
imapClient: imapClient, imapClient: imapClient,
pgpMailer: pgpMailer, pgpMailer: pgpMailer,
ignoreUploadOnSent: self._emailDao.checkIgnoreUploadOnSent(credentials.imap.host) ignoreUploadOnSent: self._emailDao.checkIgnoreUploadOnSent(credentials.imap.host)
}, callback); });
} }).then(callback).catch(callback);
function onConnectionError(error) { function onConnectionError(error) {
axe.debug('Connection error. Attempting reconnect in ' + config.reconnectInterval + ' ms. Error: ' + (error.errMsg || error.message) + (error.stack ? ('\n' + error.stack) : '')); 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. * Event handler that is called when the user agent goes offline.
*/ */
Account.prototype.onDisconnect = function() { Account.prototype.onDisconnect = function() {
this._emailDao.onDisconnect(); return this._emailDao.onDisconnect();
}; };
/** /**
@ -211,28 +178,18 @@ Account.prototype.onDisconnect = function() {
*/ */
Account.prototype.logout = function() { Account.prototype.logout = function() {
var self = this; var self = this;
// clear app config store // clear app config store
self._auth.logout(function(err) { return self._auth.logout().then(function() {
if (err) {
self._dialog.error(err);
return;
}
// delete instance of imap-client and pgp-mailer // delete instance of imap-client and pgp-mailer
self._emailDao.onDisconnect(function(err) { return self._emailDao.onDisconnect();
if (err) {
self._dialog.error(err);
return;
}
if (typeof window.chrome !== 'undefined' && chrome.runtime && chrome.runtime.reload) { }).then(function() {
// reload chrome app if (typeof window.chrome !== 'undefined' && chrome.runtime && chrome.runtime.reload) {
chrome.runtime.reload(); // reload chrome app
} else { chrome.runtime.reload();
// navigate to login } else {
window.location.href = '/'; // navigate to login
} window.location.href = '/';
}); }
}); });
}; };

View File

@ -310,9 +310,10 @@ Auth.prototype._loadCredentials = function() {
/** /**
* Handles certificate updates and errors by notifying the user. * Handles certificate updates and errors by notifying the user.
* @param {String} component Either imap or smtp * @param {String} component Either imap or smtp
* @param {Function} callback The error handler
* @param {[type]} pemEncodedCert The PEM encoded SSL certificate * @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; var self = this;
axe.debug('new ssl certificate received: ' + pemEncodedCert); 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 // no previous ssl cert, trust on first use
self[component].ca = pemEncodedCert; self[component].ca = pemEncodedCert;
self.credentialsDirty = true; self.credentialsDirty = true;
return self.storeCredentials(); self.storeCredentials().then(callback).catch(callback);
return;
} }
if (self[component].ca === pemEncodedCert) { 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 // previous ssl cert known, does not match: query user and certificate
return new Promise(function() { callback({
throw { title: str.updateCertificateTitle,
title: str.updateCertificateTitle, message: str.updateCertificateMessage.replace('{0}', self[component].host),
message: str.updateCertificateMessage.replace('{0}', self[component].host), positiveBtnStr: str.updateCertificatePosBtn,
positiveBtnStr: str.updateCertificatePosBtn, negativeBtnStr: str.updateCertificateNegBtn,
negativeBtnStr: str.updateCertificateNegBtn, showNegativeBtn: true,
showNegativeBtn: true, faqLink: str.certificateFaqLink,
faqLink: str.certificateFaqLink, callback: function(granted) {
callback: function(granted) { if (!granted) {
if (!granted) { return;
return;
}
self[component].ca = pemEncodedCert;
self.credentialsDirty = true;
return self.storeCredentials().then(function() {
return onConnect();
});
} }
};
self[component].ca = pemEncodedCert;
self.credentialsDirty = true;
self.storeCredentials().then(function() {
onConnect(callback);
}).catch(callback);
}
}); });
}; };

View File

@ -58,55 +58,45 @@ describe('Account Service unit test', function() {
account.init({ account.init({
emailAddress: dummyUser.replace('@'), emailAddress: dummyUser.replace('@'),
realname: realname realname: realname
}, onInit); }).catch(function onInit(err) {
function onInit(err, keys) {
expect(err).to.exist; expect(err).to.exist;
expect(keys).to.not.exist; });
}
}); });
it('should fail for _accountStore.init', function() { it('should fail for _accountStore.init', function() {
devicestorageStub.init.yields(new Error('asdf')); devicestorageStub.init.returns(rejects(new Error('asdf')));
account.init({ account.init({
emailAddress: dummyUser, emailAddress: dummyUser,
realname: realname realname: realname
}, onInit); }).catch(function onInit(err) {
function onInit(err, keys) {
expect(err.message).to.match(/asdf/); expect(err.message).to.match(/asdf/);
expect(keys).to.not.exist; });
}
}); });
it('should fail for _updateHandler.update', function() { 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({ account.init({
emailAddress: dummyUser, emailAddress: dummyUser,
realname: realname realname: realname
}, onInit); }).catch(function onInit(err) {
function onInit(err, keys) {
expect(err.message).to.match(/Updating/); expect(err.message).to.match(/Updating/);
expect(keys).to.not.exist; });
}
}); });
it('should fail for _keychain.getUserKeyPair', function() { it('should fail for _keychain.getUserKeyPair', function() {
updateHandlerStub.update.yields(); devicestorageStub.init.returns(resolves());
keychainStub.getUserKeyPair.yields(new Error('asdf')); updateHandlerStub.update.returns(resolves());
keychainStub.getUserKeyPair.returns(rejects(new Error('asdf')));
account.init({ account.init({
emailAddress: dummyUser, emailAddress: dummyUser,
realname: realname realname: realname
}, onInit); }).catch(function(err) {
function onInit(err, keys) {
expect(err.message).to.match(/asdf/); expect(err.message).to.match(/asdf/);
expect(keys).to.not.exist; });
}
}); });
it('should fail for _keychain.refreshKeyForUserId', function() { it('should fail for _keychain.refreshKeyForUserId', function() {
@ -114,19 +104,17 @@ describe('Account Service unit test', function() {
publicKey: 'publicKey' publicKey: 'publicKey'
}; };
updateHandlerStub.update.yields(); devicestorageStub.init.returns(resolves());
keychainStub.getUserKeyPair.yields(null, storedKeys); updateHandlerStub.update.returns(resolves());
keychainStub.refreshKeyForUserId.yields(new Error('asdf')); keychainStub.getUserKeyPair.returns(resolves(storedKeys));
keychainStub.refreshKeyForUserId.returns(rejects(new Error('asdf')));
account.init({ account.init({
emailAddress: dummyUser, emailAddress: dummyUser,
realname: realname realname: realname
}, onInit); }).catch(function(err) {
function onInit(err, keys) {
expect(err.message).to.match(/asdf/); expect(err.message).to.match(/asdf/);
expect(keys).to.not.exist; });
}
}); });
it('should fail for _emailDao.init after _keychain.refreshKeyForUserId', function() { it('should fail for _emailDao.init after _keychain.refreshKeyForUserId', function() {
@ -134,20 +122,18 @@ describe('Account Service unit test', function() {
publicKey: 'publicKey' publicKey: 'publicKey'
}; };
updateHandlerStub.update.yields(); devicestorageStub.init.returns(resolves());
keychainStub.getUserKeyPair.yields(null, storedKeys); updateHandlerStub.update.returns(resolves());
keychainStub.refreshKeyForUserId.yields(null, storedKeys); keychainStub.getUserKeyPair.returns(resolves(storedKeys));
emailStub.init.yields(new Error('asdf')); keychainStub.refreshKeyForUserId.returns(resolves(storedKeys));
emailStub.init.returns(rejects(new Error('asdf')));
account.init({ account.init({
emailAddress: dummyUser, emailAddress: dummyUser,
realname: realname realname: realname
}, onInit); }).catch(function(err) {
function onInit(err, keys) {
expect(err.message).to.match(/asdf/); expect(err.message).to.match(/asdf/);
expect(keys).to.not.exist; });
}
}); });
it('should fail for _emailDao.init', function() { it('should fail for _emailDao.init', function() {
@ -156,19 +142,17 @@ describe('Account Service unit test', function() {
privateKey: 'privateKey' privateKey: 'privateKey'
}; };
updateHandlerStub.update.yields(); devicestorageStub.init.returns(resolves());
keychainStub.getUserKeyPair.yields(null, storedKeys); updateHandlerStub.update.returns(resolves());
emailStub.init.yields(new Error('asdf')); keychainStub.getUserKeyPair.returns(resolves(storedKeys));
emailStub.init.returns(rejects(new Error('asdf')));
account.init({ account.init({
emailAddress: dummyUser, emailAddress: dummyUser,
realname: realname realname: realname
}, onInit); }).catch(function(err) {
function onInit(err, keys) {
expect(err.message).to.match(/asdf/); expect(err.message).to.match(/asdf/);
expect(keys).to.not.exist; });
}
}); });
it('should work after _keychain.refreshKeyForUserId', function() { it('should work after _keychain.refreshKeyForUserId', function() {
@ -176,20 +160,20 @@ describe('Account Service unit test', function() {
publicKey: 'publicKey' publicKey: 'publicKey'
}; };
updateHandlerStub.update.yields(); devicestorageStub.init.returns(resolves());
keychainStub.getUserKeyPair.yields(null, storedKeys); updateHandlerStub.update.returns(resolves());
keychainStub.refreshKeyForUserId.yields(null, 'publicKey'); keychainStub.getUserKeyPair.returns(resolves(storedKeys));
emailStub.init.yields(); keychainStub.refreshKeyForUserId.returns(resolves('publicKey'));
emailStub.init.returns(resolves());
account.init({ account.init({
emailAddress: dummyUser, emailAddress: dummyUser,
realname: realname realname: realname
}, onInit); }, function onInit(keys) {
function onInit(err, keys) {
expect(err).to.not.exist;
expect(keys).to.deep.equal(storedKeys); expect(keys).to.deep.equal(storedKeys);
} expect(keychainStub.refreshKeyForUserId.calledOnce).to.be.true;
expect(emailStub.init.calledOnce).to.be.true;
});
}); });
it('should work', function() { it('should work', function() {
@ -198,19 +182,20 @@ describe('Account Service unit test', function() {
privateKey: 'privateKey' privateKey: 'privateKey'
}; };
updateHandlerStub.update.yields(); devicestorageStub.init.returns(resolves());
keychainStub.getUserKeyPair.yields(null, storedKeys); updateHandlerStub.update.returns(resolves());
emailStub.init.yields(); keychainStub.getUserKeyPair.returns(resolves(storedKeys));
emailStub.init.returns(resolves());
account.init({ account.init({
emailAddress: dummyUser, emailAddress: dummyUser,
realname: realname realname: realname
}, onInit); }, function onInit(keys) {
function onInit(err, keys) {
expect(err).to.not.exist;
expect(keys).to.equal(storedKeys); 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(); account.isOnline.restore();
}); });
it('should fail due to _auth.getCredentials', function() { it('should fail due to _auth.getCredentials', function(done) {
authStub.getCredentials.yields(new Error('asdf')); authStub.getCredentials.returns(rejects(new Error('asdf')));
dialogStub.error = function(err) {
expect(err.message).to.match(/asdf/);
done();
};
account.onConnect(); account.onConnect();
expect(dialogStub.error.calledOnce).to.be.true;
}); });
it('should work', function() { it('should fail due to _auth.getCredentials', function(done) {
authStub.getCredentials.yields(null, credentials); authStub.getCredentials.returns(rejects(new Error('asdf')));
emailStub.onConnect.yields();
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; it('should work', function(done) {
expect(dialogStub.error.calledOnce).to.be.true; 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() { describe('onDisconnect', function() {
it('should work', function() { it('should work', function(done) {
account.onDisconnect(); emailStub.onDisconnect.returns(resolves());
expect(emailStub.onDisconnect.calledOnce).to.be.true; account.onDisconnect().then(done);
}); });
}); });
describe('logout', function() { describe('logout', function() {
it('should fail due to _auth.logout', function() { it('should fail due to _auth.logout', function(done) {
authStub.logout.yields(new Error()); authStub.logout.returns(rejects(new Error('asdf')));
account.logout(); account.logout().catch(function(err) {
expect(err.message).to.match(/asdf/);
expect(dialogStub.error.calledOnce).to.be.true; done();
});
}); });
it('should fail due to _emailDao.onDisconnect', function() { it('should fail due to _emailDao.onDisconnect', function(done) {
authStub.logout.yields(); authStub.logout.returns(resolves());
emailStub.onDisconnect.yields(new Error()); emailStub.onDisconnect.returns(rejects(new Error('asdf')));
account.logout(); account.logout().catch(function(err) {
expect(err.message).to.match(/asdf/);
expect(dialogStub.error.calledOnce).to.be.true; done();
});
}); });
}); });

View File

@ -311,7 +311,8 @@ describe('Auth unit tests', function() {
expect(storeCredentialsStub.callCount).to.equal(1); expect(storeCredentialsStub.callCount).to.equal(1);
done(); done();
} }
auth.handleCertificateUpdate('imap', onConnectDummy, callback, dummyCert).then(callback);
auth.handleCertificateUpdate('imap', onConnectDummy, callback, dummyCert);
}); });
it('should work for stored cert', function() { it('should work for stored cert', function() {
@ -320,7 +321,10 @@ describe('Auth unit tests', function() {
}; };
storeCredentialsStub.returns(resolves()); storeCredentialsStub.returns(resolves());
auth.handleCertificateUpdate('imap', onConnectDummy, dummyCert); function callback() {}
auth.handleCertificateUpdate('imap', onConnectDummy, callback, dummyCert);
expect(storeCredentialsStub.callCount).to.equal(0); expect(storeCredentialsStub.callCount).to.equal(0);
}); });
@ -337,7 +341,8 @@ describe('Auth unit tests', function() {
expect(storeCredentialsStub.callCount).to.equal(0); expect(storeCredentialsStub.callCount).to.equal(0);
done(); done();
} }
auth.handleCertificateUpdate('imap', onConnectDummy, dummyCert).catch(callback);
auth.handleCertificateUpdate('imap', onConnectDummy, callback, dummyCert);
}); });
it('should work for updated cert', function(done) { it('should work for updated cert', function(done) {
@ -351,14 +356,18 @@ describe('Auth unit tests', function() {
expect(err).to.exist; expect(err).to.exist;
expect(err.message).to.exist; expect(err.message).to.exist;
expect(storeCredentialsStub.callCount).to.equal(0); expect(storeCredentialsStub.callCount).to.equal(0);
err.callback(true).then(function() { err.callback(true);
expect(storeCredentialsStub.callCount).to.equal(1); } else {
done(); expect(storeCredentialsStub.callCount).to.equal(1);
}); done();
} }
} }
auth.handleCertificateUpdate('imap', onConnectDummy, dummyCert).catch(callback); function onConnect(cb) {
cb();
}
auth.handleCertificateUpdate('imap', onConnect, callback, dummyCert);
}); });
}); });