From 81a56a77c045dd2caace9165457d2ca73b752c6b Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Thu, 21 Nov 2013 17:37:07 +0100 Subject: [PATCH] [WO-57] Introduce encrypted outbox The outbox is encrypted using the sender's keys. Prior to sending, every mail is buffered in the outbox. --- src/js/app-controller.js | 6 +- src/js/bo/outbox.js | 55 ++++++++++---- src/js/controller/mail-list.js | 97 +++++++++++++++++-------- src/js/controller/navigation.js | 2 +- src/js/controller/write.js | 4 +- src/js/dao/email-dao.js | 84 +++++++++++++++++++--- test/new-unit/email-dao-test.js | 49 +++++++++++++ test/new-unit/mail-list-ctrl-test.js | 104 +++++++++++++++++++++++++-- test/new-unit/outbox-bo-test.js | 58 ++++++++++----- test/new-unit/write-ctrl-test.js | 19 +++-- 10 files changed, 386 insertions(+), 92 deletions(-) diff --git a/src/js/app-controller.js b/src/js/app-controller.js index 2649e67..2944ae4 100644 --- a/src/js/app-controller.js +++ b/src/js/app-controller.js @@ -155,7 +155,7 @@ define(function(require) { */ self.init = function(userId, token, callback) { var auth, imapOptions, smtpOptions, certificate, - lawnchairDao, restDao, pubkeyDao, invitationDao, + lawnchairDao, restDao, pubkeyDao, invitationDao, emailDao, keychain, imapClient, smtpClient, pgp, userStorage, xhr; // fetch pinned local ssl certificate @@ -211,10 +211,10 @@ define(function(require) { smtpClient = new SmtpClient(smtpOptions); pgp = new PGP(); userStorage = new DeviceStorageDAO(lawnchairDao); - self._emailDao = new EmailDAO(keychain, imapClient, smtpClient, pgp, userStorage); + self._emailDao = emailDao = new EmailDAO(keychain, imapClient, smtpClient, pgp, userStorage); invitationDao = new InvitationDAO(restDao); - self._outboxBo = new OutboxBO(self._emailDao, invitationDao); + self._outboxBo = new OutboxBO(emailDao, keychain, userStorage, invitationDao); // init email dao var account = { diff --git a/src/js/bo/outbox.js b/src/js/bo/outbox.js index 64a7a45..f488678 100644 --- a/src/js/bo/outbox.js +++ b/src/js/bo/outbox.js @@ -12,10 +12,29 @@ define(function(require) { * The local outbox takes care of the emails before they are being sent. * It also checks periodically if there are any mails in the local device storage to be sent. */ - var OutboxBO = function(emailDao, invitationDao) { - this._emailDao = emailDao; - this._invitationDao = invitationDao; + var OutboxBO = function(email, keychain, devicestorage, invitation) { + /** @private */ + this._email = email; + + /** @private */ + this._keychain = keychain; + + /** @private */ + this._devicestorage = devicestorage; + + + /** @private */ + this._invitation = invitation; + + /** + * Semaphore-esque flag to avoid 'concurrent' calls to _processOutbox when the timeout fires, but a call is still in process. + * @private */ this._outboxBusy = false; + + /** + * Pending, unsent emails stored in the outbox. Updated on each call to _processOutbox + * @public */ + this.pendingEmails = []; }; /** @@ -60,7 +79,7 @@ define(function(require) { self._outboxBusy = true; // get last item from outbox - self._emailDao._devicestorage.listItems(dbType, 0, null, function(err, pending) { + self._email.list(function(err, pending) { if (err) { self._outboxBusy = false; callback(err); @@ -70,6 +89,9 @@ define(function(require) { // update outbox folder count emails = pending; + // keep an independent shallow copy of the pending mails array in the member + self.pendingEmails = pending.slice(); + // sending pending mails processMails(); }); @@ -80,12 +102,12 @@ define(function(require) { if (emails.length === 0) { // in the navigation controller, this updates the folder count self._outboxBusy = false; - callback(null, 0); + callback(null, self.pendingEmails.length); return; } // in the navigation controller, this updates the folder count - callback(null, emails.length); + callback(null, self.pendingEmails.length); var email = emails.shift(); checkReceivers(email); } @@ -107,7 +129,7 @@ define(function(require) { // find out if there are unregistered users email.to.forEach(function(recipient) { - self._emailDao._keychain.getReceiverPublicKey(recipient.address, function(err, key) { + self._keychain.getReceiverPublicKey(recipient.address, function(err, key) { if (err) { self._outboxBusy = false; callback(err); @@ -125,7 +147,7 @@ define(function(require) { // invite the unregistered receivers, if necessary function invite(addresses) { - var sender = self._emailDao._account.emailAddress; + var sender = self._email._account.emailAddress; var invitationFinished = _.after(addresses.length, function() { // after all of the invitations are checked and sent (if necessary), @@ -136,7 +158,7 @@ define(function(require) { addresses.forEach(function(recipient) { var recipientAddress = recipient.address; - self._invitationDao.check({ + self._invitation.check({ recipient: recipientAddress, sender: sender }, function(err, status) { @@ -153,7 +175,7 @@ define(function(require) { } // the recipient is not yet invited, so let's do that - self._invitationDao.invite({ + self._invitation.invite({ recipient: recipientAddress, sender: sender }, function(err, status) { @@ -187,7 +209,7 @@ define(function(require) { }; // send invitation mail - self._emailDao.send(invitationMail, function(err) { + self._email.send(invitationMail, function(err) { if (err) { self._outboxBusy = false; callback(err); @@ -199,7 +221,8 @@ define(function(require) { } function sendEncrypted(email) { - self._emailDao.encryptedSend(email, function(err) { + removeFromPendingMails(email); + self._email.encryptedSend(email, function(err) { if (err) { self._outboxBusy = false; callback(err); @@ -210,6 +233,12 @@ define(function(require) { }); } + // update the member so that the outbox can visualize + function removeFromPendingMails(email) { + var i = self.pendingEmails.indexOf(email); + self.pendingEmails.splice(i, 1); + } + function removeFromStorage(id) { if (!id) { self._outboxBusy = false; @@ -221,7 +250,7 @@ define(function(require) { // delete email from local storage var key = dbType + '_' + id; - self._emailDao._devicestorage.removeList(key, function(err) { + self._devicestorage.removeList(key, function(err) { if (err) { self._outboxBusy = false; callback(err); diff --git a/src/js/controller/mail-list.js b/src/js/controller/mail-list.js index b5623de..1de286d 100644 --- a/src/js/controller/mail-list.js +++ b/src/js/controller/mail-list.js @@ -7,7 +7,7 @@ define(function(require) { IScroll = require('iscroll'), str = require('js/app-config').string, cfg = require('js/app-config').config, - emailDao; + emailDao, outboxBo; var MailListCtrl = function($scope) { var offset = 0, @@ -19,6 +19,8 @@ define(function(require) { // emailDao = appController._emailDao; + outboxBo = appController._outboxBo; + if (emailDao) { emailDao.onIncomingMessage = function(email) { if (email.subject.indexOf(str.subjectPrefix) === -1) { @@ -67,6 +69,13 @@ define(function(require) { }; $scope.synchronize = function(callback) { + // if we're in the outbox, don't do an imap sync + if (getFolder().type === 'Outbox') { + updateStatus('Last update: ', new Date()); + displayEmails(outboxBo.pendingEmails); + return; + } + updateStatus('Syncing ...'); // sync from imap to local db syncImapFolder({ @@ -93,13 +102,26 @@ define(function(require) { return; } - var index, trashFolder; + var index, currentFolder, trashFolder, outboxFolder; + + currentFolder = getFolder(); trashFolder = _.findWhere($scope.folders, { type: 'Trash' }); - if (getFolder() === trashFolder) { + outboxFolder = _.findWhere($scope.folders, { + type: 'Outbox' + }); + + if (currentFolder === outboxFolder) { + $scope.onError({ + errMsg: 'Deleting messages from the outbox is not yet supported.' + }); + return; + } + + if (currentFolder === trashFolder) { $scope.state.dialog = { open: true, title: 'Delete', @@ -159,22 +181,44 @@ define(function(require) { } }; - $scope.$watch('state.nav.currentFolder', function() { + $scope._stopWatchTask = $scope.$watch('state.nav.currentFolder', function() { if (!getFolder()) { return; } - // production... in chrome packaged app - if (window.chrome && chrome.identity) { - initList(); + // development... display dummy mail objects + if (!window.chrome || !chrome.identity) { + firstSelect = true; + updateStatus('Last update: ', new Date()); + $scope.emails = createDummyMails(); + $scope.select($scope.emails[0]); return; } - // development... display dummy mail objects - firstSelect = true; - updateStatus('Last update: ', new Date()); - $scope.emails = createDummyMails(); - $scope.select($scope.emails[0]); + // production... in chrome packaged app + + // if we're in the outbox, read directly from there. + if (getFolder().type === 'Outbox') { + updateStatus('Last update: ', new Date()); + displayEmails(outboxBo.pendingEmails); + return; + } + + updateStatus('Read cache ...'); + + // list messaged from local db + listLocalMessages({ + folder: getFolder().path, + offset: offset, + num: num + }, function sync() { + updateStatus('Syncing ...'); + $scope.$apply(); + + // sync imap folder to local db + $scope.synchronize(); + }); + }); // share local scope functions with root state @@ -196,23 +240,6 @@ define(function(require) { }, function() {}); } - function initList() { - updateStatus('Read cache ...'); - - // list messaged from local db - listLocalMessages({ - folder: getFolder().path, - offset: offset, - num: num - }, function sync() { - updateStatus('Syncing ...'); - $scope.$apply(); - - // sync imap folder to local db - $scope.synchronize(); - }); - } - function syncImapFolder(options, callback) { emailDao.unreadMessages(getFolder().path, function(err, unreadCount) { if (err) { @@ -270,7 +297,12 @@ define(function(require) { $scope.emails = emails; $scope.select($scope.emails[0]); - $scope.$apply(); + + // syncing from the outbox is a synchronous call, so we mustn't call $scope.$apply + // for every other IMAP folder, this call is asynchronous, hence we have to call $scope.$apply... + if (getFolder().type !== 'Outbox') { + $scope.$apply(); + } } function getFolder() { @@ -278,6 +310,11 @@ define(function(require) { } function markAsRead(email) { + // marking mails as read is meaningless in the outbox + if (getFolder().type === 'Outbox') { + return; + } + // don't mark top selected email automatically if (firstSelect) { firstSelect = false; diff --git a/src/js/controller/navigation.js b/src/js/controller/navigation.js index 300a5b6..09ae961 100644 --- a/src/js/controller/navigation.js +++ b/src/js/controller/navigation.js @@ -79,7 +79,7 @@ define(function(require) { // start checking outbox periodically outboxBo.startChecking($scope.onOutboxUpdate); // make function available globally for write controller - $scope.emptyOutbox = outboxBo._processOutbox; + $scope.emptyOutbox = outboxBo._processOutbox.bind(outboxBo); callback(folders); $scope.$apply(); diff --git a/src/js/controller/write.js b/src/js/controller/write.js index a4f5138..c6ca86c 100644 --- a/src/js/controller/write.js +++ b/src/js/controller/write.js @@ -165,9 +165,7 @@ define(function(require) { }); }); - // set an id for the email and store in outbox - email.id = util.UUID(); - emailDao._devicestorage.storeList([email], 'email_OUTBOX', function(err) { + emailDao.store(email, function(err) { if (err) { $scope.onError(err); return; diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index d29de52..951e8ab 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -592,17 +592,27 @@ define(function(require) { } // public key found... encrypt and send - self.encryptForUser(email, receiverPubkey.publicKey, callback); + self.encryptForUser({ + email: email, + receiverPubkey: receiverPubkey.publicKey + }, function(err, email) { + if (err) { + callback(err); + return; + } + + self.send(email, callback); + }); }); }; /** * Encrypt an email asymmetrically for an exisiting user with their public key */ - EmailDAO.prototype.encryptForUser = function(email, receiverPubkey, callback) { + EmailDAO.prototype.encryptForUser = function(options, callback) { var self = this, - pt = email.body, - receiverPubkeys = [receiverPubkey]; + pt = options.email.body, + receiverPubkeys = options.receiverPubkey ? [options.receiverPubkey] : []; // get own public key so send message can be read self._crypto.exportKeys(function(err, ownKeys) { @@ -621,9 +631,8 @@ define(function(require) { } // bundle encrypted email together for sending - frameEncryptedMessage(email, ct); - - self.send(email, callback); + frameEncryptedMessage(options.email, ct); + callback(null, options.email); }); }); }; @@ -631,7 +640,6 @@ define(function(require) { /** * Frames an encrypted message in base64 Format. */ - function frameEncryptedMessage(email, ct) { var to, greeting; @@ -645,8 +653,6 @@ define(function(require) { // build encrypted text body email.body = greeting + MESSAGE + ct + SIGNATURE; email.subject = str.subjectPrefix + email.subject; - - return email; } /** @@ -658,5 +664,63 @@ define(function(require) { self._smtpClient.send(email, callback); }; + EmailDAO.prototype.store = function(email, callback) { + var self = this, + dbType = 'email_OUTBOX'; + + email.id = util.UUID(); + + // encrypt + self.encryptForUser({ + email: email + }, function(err, email) { + if (err) { + callback(err); + return; + } + + // store to local storage + self._devicestorage.storeList([email], dbType, callback); + }); + }; + + EmailDAO.prototype.list = function(callback) { + var self = this, + dbType = 'email_OUTBOX'; + + self._devicestorage.listItems(dbType, 0, null, function(err, mails) { + if (err) { + callback(err); + return; + } + + if (mails.length === 0) { + callback(null, []); + return; + } + + self._crypto.exportKeys(function(err, ownKeys) { + if (err) { + callback(err); + return; + } + + var after = _.after(mails.length, function() { + callback(null, mails); + }); + + mails.forEach(function(mail) { + mail.body = str.cryptPrefix + mail.body.split(str.cryptPrefix)[1].split(str.cryptSuffix)[0] + str.cryptSuffix; + self._crypto.decrypt(mail.body, ownKeys.publicKeyArmored, function(err, decrypted) { + mail.body = err ? err.errMsg : decrypted; + mail.subject = mail.subject.split(str.subjectPrefix)[1]; + after(); + }); + }); + + }); + }); + }; + return EmailDAO; }); \ No newline at end of file diff --git a/test/new-unit/email-dao-test.js b/test/new-unit/email-dao-test.js index c3d1407..def4091 100644 --- a/test/new-unit/email-dao-test.js +++ b/test/new-unit/email-dao-test.js @@ -785,6 +785,55 @@ define(function(require) { }); }); + describe('store', function() { + it('should work', function(done) { + pgpStub.exportKeys.yields(null, { + publicKeyArmored: 'omgsocrypto' + }); + pgpStub.encrypt.yields(null, 'asdfasfd'); + devicestorageStub.storeList.yields(); + + emailDao.store(dummyMail, function(err) { + expect(err).to.not.exist; + expect(pgpStub.exportKeys.calledOnce).to.be.true; + expect(pgpStub.encrypt.calledOnce).to.be.true; + expect(devicestorageStub.storeList.calledOnce).to.be.true; + + done(); + }); + }); + }); + + describe('list', function() { + it('should work', function(done) { + devicestorageStub.listItems.yields(null, [{ + body: app.string.cryptPrefix + btoa('asdf') + app.string.cryptSuffix, + subject: '[whiteout] ZOMG!' + }, { + body: app.string.cryptPrefix + btoa('asdf') + app.string.cryptSuffix, + subject: '[whiteout] WTF!' + }]); + pgpStub.exportKeys.yields(null, { + publicKeyArmored: 'omgsocrypto' + }); + pgpStub.decrypt.yields(null, 'asdfasfd'); + + emailDao.list(function(err, mails) { + expect(err).to.not.exist; + + expect(devicestorageStub.listItems.calledOnce).to.be.true; + expect(pgpStub.exportKeys.calledOnce).to.be.true; + expect(pgpStub.decrypt.calledTwice).to.be.true; + expect(mails.length).to.equal(2); + expect(mails[0].body).to.equal('asdfasfd'); + expect(mails[0].subject).to.equal('ZOMG!'); + expect(mails[1].body).to.equal('asdfasfd'); + expect(mails[1].subject).to.equal('WTF!'); + + done(); + }); + }); + }); }); }); \ No newline at end of file diff --git a/test/new-unit/mail-list-ctrl-test.js b/test/new-unit/mail-list-ctrl-test.js index a4cd6b7..5c276ae 100644 --- a/test/new-unit/mail-list-ctrl-test.js +++ b/test/new-unit/mail-list-ctrl-test.js @@ -12,7 +12,7 @@ define(function(require) { describe('Mail List controller unit test', function() { var scope, ctrl, origEmailDao, emailDaoMock, keychainMock, deviceStorageMock, - emailAddress, notificationClickedHandler, + emailAddress, notificationClickedHandler, emails, hasChrome, hasNotifications, hasSocket, hasRuntime, hasIdentity; beforeEach(function() { @@ -44,6 +44,18 @@ define(function(require) { if (!hasIdentity) { window.chrome.identity = {}; } + + emails = [{ + unread: true + }, { + unread: true + }, { + unread: true + }]; + appController._outboxBo = { + pendingEmails: emails + }; + origEmailDao = appController._emailDao; emailDaoMock = sinon.createStubInstance(EmailDAO); appController._emailDao = emailDaoMock; @@ -86,7 +98,7 @@ define(function(require) { if (!hasIdentity) { delete window.chrome.identity; } - + // restore the module appController._emailDao = origEmailDao; }); @@ -105,6 +117,8 @@ define(function(require) { it('should focus mail and not mark it read', function(done) { var uid, mail, currentFolder; + scope._stopWatchTask(); + uid = 123; mail = { uid: uid, @@ -134,7 +148,6 @@ define(function(require) { expect(opts.type).to.equal('basic'); expect(opts.message).to.equal('asdasd'); expect(opts.title).to.equal('asd'); - expect(scope.state.mailList.selected).to.deep.equal(mail); expect(emailDaoMock.imapMarkMessageRead.callCount).to.equal(0); done(); }; @@ -147,6 +160,8 @@ define(function(require) { it('should focus mail and mark it read', function() { var uid, mail, currentFolder; + scope._stopWatchTask(); + uid = 123; mail = { uid: uid, @@ -172,10 +187,62 @@ define(function(require) { notificationClickedHandler('123'); // first select, irrelevant notificationClickedHandler('123'); - expect(scope.state.mailList.selected).to.deep.equal(mail); + expect(scope.state.mailList.selected).to.equal(mail); expect(emailDaoMock.imapMarkMessageRead.callCount).to.be.at.least(1); }); }); + + describe('watch task', function() { + it('should do a local list and a full imap sync and mark the first message read', function(done) { + emailDaoMock.unreadMessages.yields(null, 3); + emailDaoMock.imapSync.yields(); + emailDaoMock.listMessages.yieldsAsync(null, emails); + + scope.state.read = { + toggle: function() {} + }; + + var currentFolder = { + type: 'Inbox' + }; + scope.folders = [currentFolder]; + scope.state.nav = { + currentFolder: currentFolder + }; + + // the behavior should be async and imapMarkMessageRead is + emailDaoMock.imapMarkMessageRead = function() { + expect(scope.emails).to.deep.equal(emails); + expect(scope.state.mailList.selected).to.equal(emails[0]); + expect(emailDaoMock.unreadMessages.callCount).to.equal(2); + expect(emailDaoMock.imapSync.callCount).to.equal(2); + expect(emailDaoMock.listMessages.callCount).to.equal(3); + + done(); + }; + + scope.synchronize(); + }); + }); + + describe('synchronize', function() { + it('should read directly from outbox instead of doing a full imap sync', function() { + scope._stopWatchTask(); + + var currentFolder = { + type: 'Outbox' + }; + scope.folders = [currentFolder]; + scope.state.nav = { + currentFolder: currentFolder + }; + + scope.synchronize(); + + expect(scope.state.mailList.selected).to.equal(emails[0]); + }); + }); + describe('remove', function() { it('should not delete without a selected mail', function() { scope.remove(); @@ -183,9 +250,36 @@ define(function(require) { expect(emailDaoMock.imapDeleteMessage.called).to.be.false; }); + it('should not delete from the outbox', function(done) { + var currentFolder, mail; + + scope._stopWatchTask(); + + mail = {}; + currentFolder = { + type: 'Outbox' + }; + scope.emails = [mail]; + scope.folders = [currentFolder]; + scope.state.nav = { + currentFolder: currentFolder + }; + + scope.onError = function(err) { + expect(err).to.exist; // would normally display the notification + expect(emailDaoMock.imapDeleteMessage.called).to.be.false; + done(); + }; + + scope.remove(mail); + + }); + it('should delete the selected mail from trash folder after clicking ok', function() { var uid, mail, currentFolder; + scope._stopWatchTask(); + uid = 123; mail = { uid: uid, @@ -215,6 +309,8 @@ define(function(require) { it('should move the selected mail to the trash folder', function() { var uid, mail, currentFolder, trashFolder; + scope._stopWatchTask(); + uid = 123; mail = { uid: uid, diff --git a/test/new-unit/outbox-bo-test.js b/test/new-unit/outbox-bo-test.js index 9d1ebd1..527d449 100644 --- a/test/new-unit/outbox-bo-test.js +++ b/test/new-unit/outbox-bo-test.js @@ -18,10 +18,10 @@ define(function(require) { emailDaoStub._account = { emailAddress: dummyUser }; - emailDaoStub._devicestorage = devicestorageStub = sinon.createStubInstance(DeviceStorageDAO); - emailDaoStub._keychain = keychainStub = sinon.createStubInstance(KeychainDAO); + devicestorageStub = sinon.createStubInstance(DeviceStorageDAO); + keychainStub = sinon.createStubInstance(KeychainDAO); invitationDaoStub = sinon.createStubInstance(InvitationDAO); - outbox = new OutboxBO(emailDaoStub, invitationDaoStub); + outbox = new OutboxBO(emailDaoStub, keychainStub, devicestorageStub, invitationDaoStub); }); afterEach(function() {}); @@ -29,9 +29,12 @@ define(function(require) { describe('init', function() { it('should work', function() { expect(outbox).to.exist; - expect(outbox._emailDao).to.equal(emailDaoStub); - expect(outbox._invitationDao).to.equal(invitationDaoStub); + expect(outbox._email).to.equal(emailDaoStub); + expect(outbox._keychain).to.equal(keychainStub); + expect(outbox._devicestorage).to.equal(devicestorageStub); + expect(outbox._invitation).to.equal(invitationDaoStub); expect(outbox._outboxBusy).to.be.false; + expect(outbox.pendingEmails).to.be.empty; }); }); @@ -50,50 +53,71 @@ define(function(require) { }); describe('process outbox', function() { - it('should work', function(done) { - var dummyMails = [{ + it('should send to registered users and update pending mails', function(done) { + var member, invited, notinvited, dummyMails, unsentCount; + + member = { id: '123', to: [{ name: 'member', address: 'member@whiteout.io' }] - }, { + }; + invited = { id: '456', to: [{ name: 'invited', address: 'invited@whiteout.io' }] - }, { + }; + notinvited = { id: '789', to: [{ name: 'notinvited', address: 'notinvited@whiteout.io' }] - }]; + }; + dummyMails = [member, invited, notinvited]; - devicestorageStub.listItems.yieldsAsync(null, dummyMails); + emailDaoStub.list.yieldsAsync(null, dummyMails); emailDaoStub.encryptedSend.yieldsAsync(); emailDaoStub.send.yieldsAsync(); devicestorageStub.removeList.yieldsAsync(); - invitationDaoStub.check.withArgs(sinon.match(function(o) { return o.recipient === 'invited@whiteout.io'; })).yieldsAsync(null, InvitationDAO.INVITE_PENDING); - invitationDaoStub.check.withArgs(sinon.match(function(o) { return o.recipient === 'notinvited@whiteout.io'; })).yieldsAsync(null, InvitationDAO.INVITE_MISSING); - invitationDaoStub.invite.withArgs(sinon.match(function(o) { return o.recipient === 'notinvited@whiteout.io'; })).yieldsAsync(null, InvitationDAO.INVITE_SUCCESS); - keychainStub.getReceiverPublicKey.withArgs(sinon.match(function(o) { return o === 'member@whiteout.io'; })).yieldsAsync(null, 'this is not the key you are looking for...'); - keychainStub.getReceiverPublicKey.withArgs(sinon.match(function(o) { return o === 'invited@whiteout.io' || o === 'notinvited@whiteout.io'; })).yieldsAsync(); + invitationDaoStub.check.withArgs(sinon.match(function(o) { + return o.recipient === 'invited@whiteout.io'; + })).yieldsAsync(null, InvitationDAO.INVITE_PENDING); + invitationDaoStub.check.withArgs(sinon.match(function(o) { + return o.recipient === 'notinvited@whiteout.io'; + })).yieldsAsync(null, InvitationDAO.INVITE_MISSING); + invitationDaoStub.invite.withArgs(sinon.match(function(o) { + return o.recipient === 'notinvited@whiteout.io'; + })).yieldsAsync(null, InvitationDAO.INVITE_SUCCESS); + keychainStub.getReceiverPublicKey.withArgs(sinon.match(function(o) { + return o === 'member@whiteout.io'; + })).yieldsAsync(null, 'this is not the key you are looking for...'); + keychainStub.getReceiverPublicKey.withArgs(sinon.match(function(o) { + return o === 'invited@whiteout.io' || o === 'notinvited@whiteout.io'; + })).yieldsAsync(); var check = _.after(dummyMails.length + 1, function() { - expect(devicestorageStub.listItems.callCount).to.equal(1); + expect(unsentCount).to.equal(2); + expect(emailDaoStub.list.callCount).to.equal(1); expect(emailDaoStub.encryptedSend.callCount).to.equal(1); expect(emailDaoStub.send.callCount).to.equal(1); expect(devicestorageStub.removeList.callCount).to.equal(1); expect(invitationDaoStub.check.callCount).to.equal(2); expect(invitationDaoStub.invite.callCount).to.equal(1); + + expect(outbox.pendingEmails.length).to.equal(2); + expect(outbox.pendingEmails).to.contain(invited); + expect(outbox.pendingEmails).to.contain(notinvited); done(); }); function onOutboxUpdate(err, count) { expect(err).to.not.exist; expect(count).to.exist; + unsentCount = count; check(); } diff --git a/test/new-unit/write-ctrl-test.js b/test/new-unit/write-ctrl-test.js index 078b8d2..841d296 100644 --- a/test/new-unit/write-ctrl-test.js +++ b/test/new-unit/write-ctrl-test.js @@ -6,17 +6,17 @@ define(function(require) { mocks = require('angularMocks'), WriteCtrl = require('js/controller/write'), EmailDAO = require('js/dao/email-dao'), - DeviceStorageDAO = require('js/dao/devicestorage-dao'), KeychainDAO = require('js/dao/keychain-dao'), appController = require('js/app-controller'); describe('Write controller unit test', function() { - var ctrl, scope, origEmailDao, emailDaoMock, keychainMock, deviceStorageMock, emailAddress; + var ctrl, scope, origEmailDao, emailDaoMock, keychainMock, emailAddress; beforeEach(function() { origEmailDao = appController._emailDao; emailDaoMock = sinon.createStubInstance(EmailDAO); appController._emailDao = emailDaoMock; + emailAddress = 'fred@foo.com'; emailDaoMock._account = { emailAddress: emailAddress, @@ -25,9 +25,6 @@ define(function(require) { keychainMock = sinon.createStubInstance(KeychainDAO); emailDaoMock._keychain = keychainMock; - deviceStorageMock = sinon.createStubInstance(DeviceStorageDAO); - emailDaoMock._devicestorage = deviceStorageMock; - angular.module('writetest', []); mocks.module('writetest'); mocks.inject(function($rootScope, $controller) { @@ -174,12 +171,12 @@ define(function(require) { scope.subject = 'yaddablabla'; scope.toKey = 'Public Key'; - deviceStorageMock.storeList.withArgs(sinon.match(function(mail) { - return mail[0].from[0].address === emailAddress && mail[0].to.length === 3; + emailDaoMock.store.withArgs(sinon.match(function(mail) { + return mail.from[0].address === emailAddress && mail.to.length === 3; })).yieldsAsync(); scope.emptyOutbox = function() { expect(scope.state.writer.open).to.be.false; - expect(deviceStorageMock.storeList.calledOnce).to.be.true; + expect(emailDaoMock.store.calledOnce).to.be.true; done(); }; @@ -193,8 +190,8 @@ define(function(require) { scope.subject = 'yaddablabla'; scope.toKey = 'Public Key'; - deviceStorageMock.storeList.withArgs(sinon.match(function(mail) { - return mail[0].from[0].address === emailAddress && mail[0].to.length === 3; + emailDaoMock.store.withArgs(sinon.match(function(mail) { + return mail.from[0].address === emailAddress && mail.to.length === 3; })).yields({ errMsg: 'snafu' }); @@ -202,7 +199,7 @@ define(function(require) { scope.onError = function(err) { expect(err).to.exist; expect(scope.state.writer.open).to.be.true; - expect(deviceStorageMock.storeList.calledOnce).to.be.true; + expect(emailDaoMock.store.calledOnce).to.be.true; done(); };