1
0
mirror of https://github.com/moparisthebest/mail synced 2025-02-16 15:10:10 -05:00

Merge branch 'dev/email-dao-refactoring'

This commit is contained in:
Tankred Hase 2013-12-04 17:44:44 +01:00
commit 3790009260
20 changed files with 2645 additions and 1426 deletions

View File

@ -38,7 +38,8 @@ define(function(require) {
}, },
checkOutboxInterval: 5000, checkOutboxInterval: 5000,
iconPath: '/img/icon.png', iconPath: '/img/icon.png',
verificationUrl: '/verify/' verificationUrl: '/verify/',
verificationUuidLength: 36
}; };
/** /**

View File

@ -155,7 +155,7 @@ define(function(require) {
*/ */
self.init = function(userId, token, callback) { self.init = function(userId, token, callback) {
var auth, imapOptions, smtpOptions, certificate, var auth, imapOptions, smtpOptions, certificate,
lawnchairDao, restDao, pubkeyDao, invitationDao, lawnchairDao, restDao, pubkeyDao, invitationDao, emailDao,
keychain, imapClient, smtpClient, pgp, userStorage, xhr; keychain, imapClient, smtpClient, pgp, userStorage, xhr;
// fetch pinned local ssl certificate // fetch pinned local ssl certificate
@ -213,10 +213,10 @@ define(function(require) {
pgp = new PGP(); pgp = new PGP();
self._crypto = pgp; self._crypto = pgp;
userStorage = new DeviceStorageDAO(lawnchairDao); 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); invitationDao = new InvitationDAO(restDao);
self._outboxBo = new OutboxBO(self._emailDao, invitationDao); self._outboxBo = new OutboxBO(emailDao, keychain, userStorage, invitationDao);
// init email dao // init email dao
var account = { var account = {
@ -224,7 +224,13 @@ define(function(require) {
asymKeySize: config.asymKeySize asymKeySize: config.asymKeySize
}; };
self._emailDao.init(account, callback); self._emailDao.init({
account: account
}, function(err, keypair) {
self._outboxBo.init();
callback(err, keypair);
});
} }
}; };

View File

@ -12,10 +12,36 @@ define(function(require) {
* The local outbox takes care of the emails before they are being sent. * 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. * It also checks periodically if there are any mails in the local device storage to be sent.
*/ */
var OutboxBO = function(emailDao, invitationDao) { var OutboxBO = function(emailDao, keychain, devicestorage, invitationDao) {
/** @private */
this._emailDao = emailDao; this._emailDao = emailDao;
/** @private */
this._keychain = keychain;
/** @private */
this._devicestorage = devicestorage;
/** @private */
this._invitationDao = invitationDao; this._invitationDao = invitationDao;
/**
* Semaphore-esque flag to avoid 'concurrent' calls to _processOutbox when the timeout fires, but a call is still in process.
* @private */
this._outboxBusy = false; this._outboxBusy = false;
/**
* Pending, unsent emails stored in the outbox. Updated on each call to _processOutbox
* @public */
this.pendingEmails = [];
};
OutboxBO.prototype.init = function() {
var outboxFolder = _.findWhere(this._emailDao._account.folders, {
type: 'Outbox'
});
outboxFolder.messages = this.pendingEmails;
}; };
/** /**
@ -60,7 +86,7 @@ define(function(require) {
self._outboxBusy = true; self._outboxBusy = true;
// get last item from outbox // get last item from outbox
self._emailDao._devicestorage.listItems(dbType, 0, null, function(err, pending) { self._emailDao.list(function(err, pending) {
if (err) { if (err) {
self._outboxBusy = false; self._outboxBusy = false;
callback(err); callback(err);
@ -70,6 +96,12 @@ define(function(require) {
// update outbox folder count // update outbox folder count
emails = pending; emails = pending;
// fill all the pending mails into the pending mails array
self.pendingEmails.length = 0; //fastest way to empty an array
pending.forEach(function(i) {
self.pendingEmails.push(i);
});
// sending pending mails // sending pending mails
processMails(); processMails();
}); });
@ -80,12 +112,12 @@ define(function(require) {
if (emails.length === 0) { if (emails.length === 0) {
// in the navigation controller, this updates the folder count // in the navigation controller, this updates the folder count
self._outboxBusy = false; self._outboxBusy = false;
callback(null, 0); callback(null, self.pendingEmails.length);
return; return;
} }
// in the navigation controller, this updates the folder count // in the navigation controller, this updates the folder count
callback(null, emails.length); callback(null, self.pendingEmails.length);
var email = emails.shift(); var email = emails.shift();
checkReceivers(email); checkReceivers(email);
} }
@ -107,7 +139,7 @@ define(function(require) {
// find out if there are unregistered users // find out if there are unregistered users
email.to.forEach(function(recipient) { email.to.forEach(function(recipient) {
self._emailDao._keychain.getReceiverPublicKey(recipient.address, function(err, key) { self._keychain.getReceiverPublicKey(recipient.address, function(err, key) {
if (err) { if (err) {
self._outboxBusy = false; self._outboxBusy = false;
callback(err); callback(err);
@ -187,7 +219,7 @@ define(function(require) {
}; };
// send invitation mail // send invitation mail
self._emailDao.send(invitationMail, function(err) { self._emailDao.sendPlaintext(invitationMail, function(err) {
if (err) { if (err) {
self._outboxBusy = false; self._outboxBusy = false;
callback(err); callback(err);
@ -199,7 +231,10 @@ define(function(require) {
} }
function sendEncrypted(email) { function sendEncrypted(email) {
self._emailDao.encryptedSend(email, function(err) { removeFromPendingMails(email);
self._emailDao.sendEncrypted({
email: email
}, function(err) {
if (err) { if (err) {
self._outboxBusy = false; self._outboxBusy = false;
callback(err); callback(err);
@ -210,6 +245,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) { function removeFromStorage(id) {
if (!id) { if (!id) {
self._outboxBusy = false; self._outboxBusy = false;
@ -221,7 +262,7 @@ define(function(require) {
// delete email from local storage // delete email from local storage
var key = dbType + '_' + id; var key = dbType + '_' + id;
self._emailDao._devicestorage.removeList(key, function(err) { self._devicestorage.removeList(key, function(err) {
if (err) { if (err) {
self._outboxBusy = false; self._outboxBusy = false;
callback(err); callback(err);

View File

@ -37,7 +37,11 @@ define(function(require) {
handleError(err); handleError(err);
return; return;
} }
emailDao.unlock(keypair, $scope.passphrase, onUnlock);
emailDao.unlock({
keypair: keypair,
passphrase: $scope.passphrase
}, onUnlock);
}); });
} }

View File

@ -39,7 +39,10 @@ define(function(require) {
encryptedKey: $scope.key.privateKeyArmored encryptedKey: $scope.key.privateKeyArmored
}; };
emailDao.unlock(keypair, $scope.passphrase, function(err) { emailDao.unlock({
keypair: keypair,
passphrase: $scope.passphrase
}, function(err) {
if (err) { if (err) {
$scope.incorrect = true; $scope.incorrect = true;
$scope.onError(err); $scope.onError(err);

View File

@ -45,15 +45,7 @@ define(function(require) {
return; return;
} }
// login to imap backend redirect(availableKeys);
appController._emailDao.imapLogin(function(err) {
if (err) {
$scope.onError(err);
return;
}
redirect(availableKeys);
});
}); });
}); });
} }

View File

@ -1,24 +1,23 @@
define(function(require) { define(function(require) {
'use strict'; 'use strict';
var _ = require('underscore'), var angular = require('angular'),
angular = require('angular'), _ = require('underscore'),
appController = require('js/app-controller'), appController = require('js/app-controller'),
IScroll = require('iscroll'), IScroll = require('iscroll'),
str = require('js/app-config').string, str = require('js/app-config').string,
cfg = require('js/app-config').config, cfg = require('js/app-config').config,
emailDao; emailDao, outboxBo;
var MailListCtrl = function($scope) { var MailListCtrl = function($scope) {
var offset = 0,
num = 100,
firstSelect = true;
// //
// Init // Init
// //
emailDao = appController._emailDao; emailDao = appController._emailDao;
outboxBo = appController._outboxBo;
// push handler
if (emailDao) { if (emailDao) {
emailDao.onIncomingMessage = function(email) { emailDao.onIncomingMessage = function(email) {
if (email.subject.indexOf(str.subjectPrefix) === -1) { if (email.subject.indexOf(str.subjectPrefix) === -1) {
@ -41,7 +40,7 @@ define(function(require) {
return; return;
} }
email = _.findWhere($scope.emails, { email = _.findWhere(getFolder().messages, {
uid: uid uid: uid
}); });
@ -62,29 +61,39 @@ define(function(require) {
$scope.state.mailList.selected = email; $scope.state.mailList.selected = email;
// mark selected message as 'read' // // mark selected message as 'read'
markAsRead(email); // markAsRead(email);
}; };
$scope.synchronize = function(callback) { $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 ...'); updateStatus('Syncing ...');
// sync from imap to local db
syncImapFolder({ // let email dao handle sync transparently
folder: getFolder().path, emailDao.sync({
offset: -num, folder: getFolder().path
num: offset }, function(err) {
}, function() { if (err) {
// list again from local db after syncing updateStatus('Error on sync!');
listLocalMessages({ $scope.onError(err);
folder: getFolder().path, return;
offset: offset, }
num: num
}, function() { // sort emails
updateStatus('Last update: ', new Date()); displayEmails(getFolder().messages);
if (callback) { // display last update
callback(); updateStatus('Last update: ', new Date());
} $scope.$apply();
});
if (callback) {
callback();
}
}); });
}; };
@ -93,88 +102,67 @@ define(function(require) {
return; return;
} }
var index, trashFolder; var index, currentFolder, outboxFolder;
trashFolder = _.findWhere($scope.folders, { currentFolder = getFolder();
type: 'Trash' // trashFolder = _.findWhere($scope.folders, {
// type: 'Trash'
// });
outboxFolder = _.findWhere($scope.account.folders, {
type: 'Outbox'
}); });
if (getFolder() === trashFolder) { if (currentFolder === outboxFolder) {
$scope.state.dialog = { $scope.onError({
open: true, errMsg: 'Deleting messages from the outbox is not yet supported.'
title: 'Delete', });
message: 'Delete this message permanently?',
callback: function(ok) {
if (!ok) {
return;
}
removeLocalAndShowNext();
removeRemote();
}
};
return; return;
} }
removeLocalAndShowNext(); removeAndShowNext();
removeRemote(); $scope.synchronize();
function removeLocalAndShowNext() { function removeAndShowNext() {
index = $scope.emails.indexOf(email); index = getFolder().messages.indexOf(email);
// show the next mail // show the next mail
if ($scope.emails.length > 1) { if (getFolder().messages.length > 1) {
// if we're about to delete the last entry of the array, show the previous (i.e. the one below in the list), // if we're about to delete the last entry of the array, show the previous (i.e. the one below in the list),
// otherwise show the next one (i.e. the one above in the list) // otherwise show the next one (i.e. the one above in the list)
$scope.select(_.last($scope.emails) === email ? $scope.emails[index - 1] : $scope.emails[index + 1]); $scope.select(_.last(getFolder().messages) === email ? getFolder().messages[index - 1] : getFolder().messages[index + 1]);
} else { } else {
// if we have only one email in the array, show nothing // if we have only one email in the array, show nothing
$scope.select(); $scope.select();
$scope.state.mailList.selected = undefined; $scope.state.mailList.selected = undefined;
} }
$scope.emails.splice(index, 1); getFolder().messages.splice(index, 1);
}
function removeRemote() {
if (getFolder() === trashFolder) {
emailDao.imapDeleteMessage({
folder: getFolder().path,
uid: email.uid
}, moved);
return;
}
emailDao.imapMoveMessage({
folder: getFolder().path,
uid: email.uid,
destination: trashFolder.path
}, moved);
}
function moved(err) {
if (err) {
$scope.emails.splice(index, 0, email);
$scope.onError(err);
return;
}
} }
}; };
$scope.$watch('state.nav.currentFolder', function() { $scope._stopWatchTask = $scope.$watch('state.nav.currentFolder', function() {
if (!getFolder()) { if (!getFolder()) {
return; return;
} }
// production... in chrome packaged app // development... display dummy mail objects
if (window.chrome && chrome.identity) { if (!window.chrome || !chrome.identity) {
initList(); updateStatus('Last update: ', new Date());
getFolder().messages = createDummyMails();
displayEmails(getFolder().messages);
return; return;
} }
// development... display dummy mail objects // production... in chrome packaged app
firstSelect = true;
updateStatus('Last update: ', new Date()); // if we're in the outbox, read directly from there.
$scope.emails = createDummyMails(); if (getFolder().type === 'Outbox') {
$scope.select($scope.emails[0]); updateStatus('Last update: ', new Date());
displayEmails(outboxBo.pendingEmails);
return;
}
displayEmails(getFolder().messages);
$scope.synchronize();
}); });
// share local scope functions with root state // share local scope functions with root state
@ -196,60 +184,6 @@ define(function(require) {
}, function() {}); }, 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) {
updateStatus('Error on sync!');
$scope.onError(err);
return;
}
// set unread count in folder model
getFolder().count = unreadCount;
$scope.$apply();
emailDao.imapSync(options, function(err) {
if (err) {
updateStatus('Error on sync!');
$scope.onError(err);
return;
}
callback();
});
});
}
function listLocalMessages(options, callback) {
firstSelect = true;
emailDao.listMessages(options, function(err, emails) {
if (err) {
updateStatus('Error listing cache!');
$scope.onError(err);
return;
}
callback(emails);
displayEmails(emails);
});
}
function updateStatus(lbl, time) { function updateStatus(lbl, time) {
$scope.lastUpdateLbl = lbl; $scope.lastUpdateLbl = lbl;
$scope.lastUpdate = (time) ? time : ''; $scope.lastUpdate = (time) ? time : '';
@ -257,59 +191,54 @@ define(function(require) {
function displayEmails(emails) { function displayEmails(emails) {
if (!emails || emails.length < 1) { if (!emails || emails.length < 1) {
$scope.emails = [];
$scope.select(); $scope.select();
$scope.$apply();
return; return;
} }
// sort by uid if (!$scope.state.mailList.selected) {
emails = _.sortBy(emails, function(e) { // select first message
return -e.uid; $scope.select(emails[emails.length - 1]);
}); }
$scope.emails = emails;
$scope.select($scope.emails[0]);
$scope.$apply();
} }
function getFolder() { function getFolder() {
return $scope.state.nav.currentFolder; return $scope.state.nav.currentFolder;
} }
function markAsRead(email) { // function markAsRead(email) {
// don't mark top selected email automatically // // marking mails as read is meaningless in the outbox
if (firstSelect) { // if (getFolder().type === 'Outbox') {
firstSelect = false; // return;
return; // }
}
$scope.state.read.toggle(true); // $scope.state.read.toggle(true);
if (!window.chrome || !chrome.socket) { // if (!window.chrome || !chrome.socket) {
return; // return;
} // }
if (!email.unread) { // if (!email.unread) {
return; // return;
} // }
email.unread = false; // email.unread = false;
emailDao.imapMarkMessageRead({ // emailDao.imapMarkMessageRead({
folder: getFolder().path, // folder: getFolder().path,
uid: email.uid // uid: email.uid
}, function(err) { // }, function(err) {
if (err) { // if (err) {
updateStatus('Error marking read!'); // updateStatus('Error marking read!');
$scope.onError(err); // $scope.onError(err);
return; // return;
} // }
}); // });
} // }
}; };
function createDummyMails() { function createDummyMails() {
var uid = 0;
var Email = function(unread, attachments, answered, html) { var Email = function(unread, attachments, answered, html) {
this.uid = '1'; this.uid = uid++;
this.from = [{ this.from = [{
name: 'Whiteout Support', name: 'Whiteout Support',
address: 'support@whiteout.io' address: 'support@whiteout.io'
@ -336,17 +265,17 @@ define(function(require) {
// //
var ngModule = angular.module('mail-list', []); var ngModule = angular.module('mail-list', []);
ngModule.directive('ngIscroll', function($parse) { ngModule.directive('ngIscroll', function() {
return { return {
link: function(scope, elm, attrs) { link: function(scope, elm, attrs) {
var model = $parse(attrs.ngIscroll); var model = attrs.ngIscroll;
scope.$watch(model, function() { scope.$watch(model, function() {
var myScroll; var myScroll;
// activate iscroll // activate iscroll
myScroll = new IScroll(elm[0], { myScroll = new IScroll(elm[0], {
mouseWheel: true mouseWheel: true
}); });
}); }, true);
} }
}; };
}); });

View File

@ -42,7 +42,7 @@ define(function(require) {
return; return;
} }
var outbox = _.findWhere($scope.folders, { var outbox = _.findWhere($scope.account.folders, {
type: 'Outbox' type: 'Outbox'
}); });
outbox.count = count; outbox.count = count;
@ -54,40 +54,29 @@ define(function(require) {
// //
// init folders // init folders
initFolders(function(folders) { initFolders();
$scope.folders = folders; // select inbox as the current folder on init
// select inbox as the current folder on init $scope.openFolder($scope.account.folders[0]);
$scope.openFolder($scope.folders[0]);
});
// //
// helper functions // helper functions
// //
function initFolders(callback) { function initFolders() {
if (window.chrome && chrome.identity) { if (window.chrome && chrome.identity) {
emailDao.imapListFolders(function(err, folders) { // get pointer to account/folder/message tree on root scope
if (err) { $scope.$root.account = emailDao._account;
$scope.onError(err);
return;
}
folders.forEach(function(f) { // start checking outbox periodically
f.count = 0; outboxBo.startChecking($scope.onOutboxUpdate);
}); // make function available globally for write controller
$scope.emptyOutbox = outboxBo._processOutbox.bind(outboxBo);
// start checking outbox periodically
outboxBo.startChecking($scope.onOutboxUpdate);
// make function available globally for write controller
$scope.emptyOutbox = outboxBo._processOutbox;
callback(folders);
$scope.$apply();
});
return; return;
} }
callback([{ // attach dummy folders for development
$scope.$root.account = {};
$scope.account.folders = [{
type: 'Inbox', type: 'Inbox',
count: 2, count: 2,
path: 'INBOX' path: 'INBOX'
@ -107,7 +96,7 @@ define(function(require) {
type: 'Trash', type: 'Trash',
count: 0, count: 0,
path: 'TRASH' path: 'TRASH'
}]); }];
} }
}; };

View File

@ -159,9 +159,7 @@ define(function(require) {
}); });
}); });
// set an id for the email and store in outbox emailDao.store(email, function(err) {
email.id = util.UUID();
emailDao._devicestorage.storeList([email], 'email_OUTBOX', function(err) {
if (err) { if (err) {
$scope.onError(err); $scope.onError(err);
return; return;
@ -185,7 +183,7 @@ define(function(require) {
$scope.replyTo.answered = true; $scope.replyTo.answered = true;
// mark remote imap object // mark remote imap object
emailDao.imapMarkAnswered({ emailDao.markAnswered({
uid: $scope.replyTo.uid, uid: $scope.replyTo.uid,
folder: $scope.state.nav.currentFolder.path folder: $scope.state.nav.currentFolder.path
}, $scope.onError); }, $scope.onError);

File diff suppressed because it is too large Load Diff

View File

@ -4,9 +4,9 @@
<h2>{{state.nav.currentFolder.type}}</h2> <h2>{{state.nav.currentFolder.type}}</h2>
</header> </header>
<div class="list-wrapper" ng-iscroll="emails"> <div class="list-wrapper" ng-iscroll="state.nav.currentFolder.messages">
<ul class="mail-list"> <ul class="mail-list">
<li ng-class="{'mail-list-active': email === state.mailList.selected, 'mail-list-attachment': email.attachments !== undefined && email.attachments.length > 0, 'mail-list-unread': email.unread && !email.answered, 'mail-list-replied': email.answered}" ng-click="select(email)" ng-repeat="email in emails"> <li ng-class="{'mail-list-active': email === state.mailList.selected, 'mail-list-attachment': email.attachments !== undefined && email.attachments.length > 0, 'mail-list-unread': email.unread && !email.answered, 'mail-list-replied': email.answered}" ng-click="select(email)" ng-repeat="email in state.nav.currentFolder.messages | orderBy:'uid':true">
<h3>{{email.from[0].name || email.from[0].address}}</h3> <h3>{{email.from[0].name || email.from[0].address}}</h3>
<div class="head"> <div class="head">
<p class="subject">{{email.subject || 'No subject'}}</p> <p class="subject">{{email.subject || 'No subject'}}</p>
@ -17,7 +17,7 @@
</ul><!--/.mail-list--> </ul><!--/.mail-list-->
</div> </div>
<footer ng-class="{syncing: lastUpdateLbl === 'Syncing ...'}" ng-click="synchronize()"> <footer ng-class="{syncing: account.busy}" ng-click="synchronize()">
<span class="spinner"></span> <span class="spinner"></span>
<span class="text">{{lastUpdateLbl}} {{lastUpdate | date:'shortTime'}}</span> <span class="text">{{lastUpdateLbl}} {{lastUpdate | date:'shortTime'}}</span>
</footer> </footer>

View File

@ -4,7 +4,7 @@
</header> </header>
<ul class="nav-primary"> <ul class="nav-primary">
<li ng-repeat="folder in folders" ng-switch="folder.count !== undefined"> <li ng-repeat="folder in account.folders" ng-switch="folder.count !== undefined">
<a href="#" ng-click="openFolder(folder); $event.preventDefault()"> <a href="#" ng-click="openFolder(folder); $event.preventDefault()">
{{folder.type}} {{folder.type}}
<span class="label-wrapper" ng-switch-when="true"><span class="label label-light">{{folder.count}}</span></span> <span class="label-wrapper" ng-switch-when="true"><span class="label label-light">{{folder.count}}</span></span>

File diff suppressed because it is too large Load Diff

View File

@ -21,8 +21,8 @@ define(function(require) {
var hasChrome, hasIdentity; var hasChrome, hasIdentity;
beforeEach(function() { beforeEach(function() {
hasChrome = !!window.chrome; hasChrome = !! window.chrome;
hasIdentity = !!window.chrome.identity; hasIdentity = !! window.chrome.identity;
window.chrome = window.chrome || {}; window.chrome = window.chrome || {};
window.chrome.identity = window.chrome.identity || {}; window.chrome.identity = window.chrome.identity || {};
@ -66,15 +66,12 @@ define(function(require) {
publicKey: 'b' publicKey: 'b'
}); });
emailDaoMock.imapLogin.yields();
angular.module('logintest', []); angular.module('logintest', []);
mocks.module('logintest'); mocks.module('logintest');
mocks.inject(function($controller, $rootScope, $location) { mocks.inject(function($controller, $rootScope, $location) {
location = $location; location = $location;
sinon.stub(location, 'path', function(path) { sinon.stub(location, 'path', function(path) {
expect(path).to.equal('/login-existing'); expect(path).to.equal('/login-existing');
expect(emailDaoMock.imapLogin.calledOnce).to.be.true;
expect(startAppStub.calledOnce).to.be.true; expect(startAppStub.calledOnce).to.be.true;
expect(checkForUpdateStub.calledOnce).to.be.true; expect(checkForUpdateStub.calledOnce).to.be.true;
expect(fetchOAuthStub.calledOnce).to.be.true; expect(fetchOAuthStub.calledOnce).to.be.true;
@ -103,15 +100,12 @@ define(function(require) {
publicKey: 'b' publicKey: 'b'
}); });
emailDaoMock.imapLogin.yields();
angular.module('logintest', []); angular.module('logintest', []);
mocks.module('logintest'); mocks.module('logintest');
mocks.inject(function($controller, $rootScope, $location) { mocks.inject(function($controller, $rootScope, $location) {
location = $location; location = $location;
sinon.stub(location, 'path', function(path) { sinon.stub(location, 'path', function(path) {
expect(path).to.equal('/login-new-device'); expect(path).to.equal('/login-new-device');
expect(emailDaoMock.imapLogin.calledOnce).to.be.true;
expect(startAppStub.calledOnce).to.be.true; expect(startAppStub.calledOnce).to.be.true;
expect(checkForUpdateStub.calledOnce).to.be.true; expect(checkForUpdateStub.calledOnce).to.be.true;
expect(fetchOAuthStub.calledOnce).to.be.true; expect(fetchOAuthStub.calledOnce).to.be.true;
@ -138,15 +132,12 @@ define(function(require) {
initStub = sinon.stub(appController, 'init'); initStub = sinon.stub(appController, 'init');
initStub.yields(); initStub.yields();
emailDaoMock.imapLogin.yields();
angular.module('logintest', []); angular.module('logintest', []);
mocks.module('logintest'); mocks.module('logintest');
mocks.inject(function($controller, $rootScope, $location) { mocks.inject(function($controller, $rootScope, $location) {
location = $location; location = $location;
sinon.stub(location, 'path', function(path) { sinon.stub(location, 'path', function(path) {
expect(path).to.equal('/login-initial'); expect(path).to.equal('/login-initial');
expect(emailDaoMock.imapLogin.calledOnce).to.be.true;
expect(startAppStub.calledOnce).to.be.true; expect(startAppStub.calledOnce).to.be.true;
expect(checkForUpdateStub.calledOnce).to.be.true; expect(checkForUpdateStub.calledOnce).to.be.true;
expect(fetchOAuthStub.calledOnce).to.be.true; expect(fetchOAuthStub.calledOnce).to.be.true;

View File

@ -71,7 +71,10 @@ define(function(require) {
pathSpy = sinon.spy(location, 'path'); pathSpy = sinon.spy(location, 'path');
scope.passphrase = passphrase; scope.passphrase = passphrase;
keychainMock.getUserKeyPair.withArgs(emailAddress).yields(null, keypair); keychainMock.getUserKeyPair.withArgs(emailAddress).yields(null, keypair);
emailDaoMock.unlock.withArgs(keypair, passphrase).yields(null); emailDaoMock.unlock.withArgs({
keypair: keypair,
passphrase: passphrase
}).yields();
scope.confirmPassphrase(); scope.confirmPassphrase();

View File

@ -93,7 +93,7 @@ define(function(require) {
_id: keyId, _id: keyId,
publicKey: 'a' publicKey: 'a'
}); });
emailDaoMock.unlock.withArgs(sinon.match.any, passphrase).yields(); emailDaoMock.unlock.yields();
keychainMock.putUserKeyPair.yields({ keychainMock.putUserKeyPair.yields({
errMsg: 'yo mamma.' errMsg: 'yo mamma.'
}); });
@ -115,7 +115,7 @@ define(function(require) {
_id: keyId, _id: keyId,
publicKey: 'a' publicKey: 'a'
}); });
emailDaoMock.unlock.withArgs(sinon.match.any, passphrase).yields({ emailDaoMock.unlock.yields({
errMsg: 'yo mamma.' errMsg: 'yo mamma.'
}); });

View File

@ -12,7 +12,7 @@ define(function(require) {
describe('Mail List controller unit test', function() { describe('Mail List controller unit test', function() {
var scope, ctrl, origEmailDao, emailDaoMock, keychainMock, deviceStorageMock, var scope, ctrl, origEmailDao, emailDaoMock, keychainMock, deviceStorageMock,
emailAddress, notificationClickedHandler, emailAddress, notificationClickedHandler, emails,
hasChrome, hasNotifications, hasSocket, hasRuntime, hasIdentity; hasChrome, hasNotifications, hasSocket, hasRuntime, hasIdentity;
beforeEach(function() { beforeEach(function() {
@ -44,6 +44,18 @@ define(function(require) {
if (!hasIdentity) { if (!hasIdentity) {
window.chrome.identity = {}; window.chrome.identity = {};
} }
emails = [{
unread: true
}, {
unread: true
}, {
unread: true
}];
appController._outboxBo = {
pendingEmails: emails
};
origEmailDao = appController._emailDao; origEmailDao = appController._emailDao;
emailDaoMock = sinon.createStubInstance(EmailDAO); emailDaoMock = sinon.createStubInstance(EmailDAO);
appController._emailDao = emailDaoMock; appController._emailDao = emailDaoMock;
@ -86,7 +98,7 @@ define(function(require) {
if (!hasIdentity) { if (!hasIdentity) {
delete window.chrome.identity; delete window.chrome.identity;
} }
// restore the module // restore the module
appController._emailDao = origEmailDao; appController._emailDao = origEmailDao;
}); });
@ -97,7 +109,7 @@ define(function(require) {
expect(scope.synchronize).to.exist; expect(scope.synchronize).to.exist;
expect(scope.remove).to.exist; expect(scope.remove).to.exist;
expect(scope.state.mailList).to.exist; expect(scope.state.mailList).to.exist;
expect(emailDaoMock.onIncomingMessage).to.exist; // expect(emailDaoMock.onIncomingMessage).to.exist;
}); });
}); });
@ -105,6 +117,8 @@ define(function(require) {
it('should focus mail and not mark it read', function(done) { it('should focus mail and not mark it read', function(done) {
var uid, mail, currentFolder; var uid, mail, currentFolder;
scope._stopWatchTask();
uid = 123; uid = 123;
mail = { mail = {
uid: uid, uid: uid,
@ -122,20 +136,12 @@ define(function(require) {
toggle: function() {} toggle: function() {}
}; };
scope.emails = [mail]; scope.emails = [mail];
emailDaoMock.imapMarkMessageRead.withArgs({ emailDaoMock.sync.yieldsAsync();
folder: currentFolder,
uid: uid
}).yields();
emailDaoMock.unreadMessages.yieldsAsync(null, 10);
emailDaoMock.imapSync.yieldsAsync();
emailDaoMock.listMessages.yieldsAsync(null, [mail]);
window.chrome.notifications.create = function(id, opts) { window.chrome.notifications.create = function(id, opts) {
expect(id).to.equal('123'); expect(id).to.equal('123');
expect(opts.type).to.equal('basic'); expect(opts.type).to.equal('basic');
expect(opts.message).to.equal('asdasd'); expect(opts.message).to.equal('asdasd');
expect(opts.title).to.equal('asd'); expect(opts.title).to.equal('asd');
expect(scope.state.mailList.selected).to.deep.equal(mail);
expect(emailDaoMock.imapMarkMessageRead.callCount).to.equal(0);
done(); done();
}; };
@ -144,77 +150,123 @@ define(function(require) {
}); });
describe('clicking push notification', function() { describe('clicking push notification', function() {
it('should focus mail and mark it read', function() { it('should focus mail', function() {
var uid, mail, currentFolder; var mail, currentFolder;
scope._stopWatchTask();
uid = 123;
mail = { mail = {
uid: uid, uid: 123,
from: [{ from: [{
address: 'asd' address: 'asd'
}], }],
subject: '[whiteout] asdasd', subject: '[whiteout] asdasd',
unread: true unread: true
}; };
currentFolder = 'asd'; currentFolder = {
type: 'asd',
messages: [mail]
};
scope.state.nav = { scope.state.nav = {
currentFolder: currentFolder currentFolder: currentFolder
}; };
scope.state.read = {
toggle: function() {}
};
scope.emails = [mail];
emailDaoMock.imapMarkMessageRead.withArgs({
folder: currentFolder,
uid: uid
}).yields();
notificationClickedHandler('123'); // first select, irrelevant
notificationClickedHandler('123'); 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('remove', function() {
it('should not delete without a selected mail', function() {
scope.remove();
expect(emailDaoMock.imapDeleteMessage.called).to.be.false; describe('synchronize', function() {
}); it('should do imap sync and display mails', function(done) {
scope._stopWatchTask();
it('should delete the selected mail from trash folder after clicking ok', function() { emailDaoMock.sync.yieldsAsync();
var uid, mail, currentFolder;
uid = 123; var currentFolder = {
mail = { type: 'Inbox',
uid: uid, messages: emails
from: [{
address: 'asd'
}],
subject: '[whiteout] asdasd',
unread: true
};
scope.emails = [mail];
currentFolder = {
type: 'Trash'
}; };
scope.folders = [currentFolder]; scope.folders = [currentFolder];
scope.state.nav = { scope.state.nav = {
currentFolder: currentFolder currentFolder: currentFolder
}; };
emailDaoMock.imapDeleteMessage.yields();
scope.remove(mail); scope.synchronize(function() {
scope.state.dialog.callback(true); expect(scope.state.nav.currentFolder.messages).to.deep.equal(emails);
expect(scope.state.mailList.selected).to.exist;
done();
});
expect(emailDaoMock.imapDeleteMessage.calledOnce).to.be.true;
expect(scope.state.mailList.selected).to.not.exist;
}); });
it('should move the selected mail to the trash folder', function() { it('should read directly from outbox instead of doing a full imap sync', function() {
var uid, mail, currentFolder, trashFolder; scope._stopWatchTask();
var currentFolder = {
type: 'Outbox'
};
scope.folders = [currentFolder];
scope.state.nav = {
currentFolder: currentFolder
};
scope.synchronize();
// emails array is also used as the outbox's pending mail
expect(scope.state.mailList.selected).to.deep.equal(emails[0]);
});
});
describe('remove', function() {
it('should not delete without a selected mail', function() {
scope.remove();
expect(emailDaoMock.sync.called).to.be.false;
});
it('should not delete from the outbox', function(done) {
var currentFolder, mail;
scope._stopWatchTask();
scope.account = {};
mail = {
uid: 123,
from: [{
address: 'asd'
}],
subject: '[whiteout] asdasd',
unread: true
};
currentFolder = {
type: 'Outbox',
path: 'OUTBOX',
messages: [mail]
};
scope.emails = [mail];
scope.account.folders = [currentFolder];
scope.state.nav = {
currentFolder: currentFolder
};
scope.onError = function(err) {
expect(err).to.exist; // would normally display the notification
expect(emailDaoMock.sync.called).to.be.false;
done();
};
scope.remove(mail);
});
it('should delete the selected mail', function() {
var uid, mail, currentFolder;
scope._stopWatchTask();
scope.account = {};
uid = 123; uid = 123;
mail = { mail = {
uid: uid, uid: uid,
@ -224,28 +276,20 @@ define(function(require) {
subject: '[whiteout] asdasd', subject: '[whiteout] asdasd',
unread: true unread: true
}; };
scope.emails = [mail];
currentFolder = { currentFolder = {
type: 'Inbox', type: 'Inbox',
path: 'INBOX' path: 'INBOX',
messages: [mail]
}; };
trashFolder = { scope.account.folders = [currentFolder];
type: 'Trash',
path: 'TRASH'
};
scope.folders = [currentFolder, trashFolder];
scope.state.nav = { scope.state.nav = {
currentFolder: currentFolder currentFolder: currentFolder
}; };
emailDaoMock.imapMoveMessage.withArgs({ emailDaoMock.sync.yields();
folder: currentFolder,
uid: uid,
destination: trashFolder.path
}).yields();
scope.remove(mail); scope.remove(mail);
expect(emailDaoMock.imapMoveMessage.calledOnce).to.be.true; expect(emailDaoMock.sync.calledOnce).to.be.true;
expect(scope.state.mailList.selected).to.not.exist; expect(scope.state.mailList.selected).to.not.exist;
}); });
}); });

View File

@ -21,15 +21,21 @@ define(function(require) {
origEmailDao = appController._emailDao; origEmailDao = appController._emailDao;
emailDaoMock = sinon.createStubInstance(EmailDAO); emailDaoMock = sinon.createStubInstance(EmailDAO);
emailDaoMock._account = {
folders: [{
type: 'Inbox',
count: 2,
path: 'INBOX'
}, {
type: 'Outbox',
count: 0,
path: 'OUTBOX'
}]
};
outboxFolder = emailDaoMock._account.folders[1];
appController._emailDao = emailDaoMock; appController._emailDao = emailDaoMock;
outboxBoMock = sinon.createStubInstance(OutboxBO); outboxBoMock = sinon.createStubInstance(OutboxBO);
appController._outboxBo = outboxBoMock; appController._outboxBo = outboxBoMock;
// for outbox checking
outboxFolder = {
type: 'Outbox'
};
emailDaoMock.imapListFolders.yields(null, [outboxFolder]);
outboxBoMock.startChecking.returns(); outboxBoMock.startChecking.returns();
angular.module('navigationtest', []); angular.module('navigationtest', []);
@ -55,7 +61,7 @@ define(function(require) {
it('should be well defined', function() { it('should be well defined', function() {
expect(scope.state).to.exist; expect(scope.state).to.exist;
expect(scope.state.nav.open).to.be.false; expect(scope.state.nav.open).to.be.false;
expect(scope.folders).to.not.be.empty; expect(scope.account.folders).to.not.be.empty;
expect(scope.onError).to.exist; expect(scope.onError).to.exist;
expect(scope.openFolder).to.exist; expect(scope.openFolder).to.exist;
@ -86,7 +92,6 @@ define(function(require) {
it('should work', function() { it('should work', function() {
var callback; var callback;
expect(emailDaoMock.imapListFolders.callCount).to.equal(1);
expect(outboxBoMock.startChecking.callCount).to.equal(1); expect(outboxBoMock.startChecking.callCount).to.equal(1);
outboxBoMock.startChecking.calledWith(sinon.match(function(cb) { outboxBoMock.startChecking.calledWith(sinon.match(function(cb) {

View File

@ -16,12 +16,16 @@ define(function(require) {
beforeEach(function() { beforeEach(function() {
emailDaoStub = sinon.createStubInstance(EmailDAO); emailDaoStub = sinon.createStubInstance(EmailDAO);
emailDaoStub._account = { emailDaoStub._account = {
emailAddress: dummyUser emailAddress: dummyUser,
folders: [{
type: 'Outbox'
}]
}; };
emailDaoStub._devicestorage = devicestorageStub = sinon.createStubInstance(DeviceStorageDAO); devicestorageStub = sinon.createStubInstance(DeviceStorageDAO);
emailDaoStub._keychain = keychainStub = sinon.createStubInstance(KeychainDAO); keychainStub = sinon.createStubInstance(KeychainDAO);
invitationDaoStub = sinon.createStubInstance(InvitationDAO); invitationDaoStub = sinon.createStubInstance(InvitationDAO);
outbox = new OutboxBO(emailDaoStub, invitationDaoStub); outbox = new OutboxBO(emailDaoStub, keychainStub, devicestorageStub, invitationDaoStub);
outbox.init();
}); });
afterEach(function() {}); afterEach(function() {});
@ -30,8 +34,12 @@ define(function(require) {
it('should work', function() { it('should work', function() {
expect(outbox).to.exist; expect(outbox).to.exist;
expect(outbox._emailDao).to.equal(emailDaoStub); expect(outbox._emailDao).to.equal(emailDaoStub);
expect(outbox._keychain).to.equal(keychainStub);
expect(outbox._devicestorage).to.equal(devicestorageStub);
expect(outbox._invitationDao).to.equal(invitationDaoStub); expect(outbox._invitationDao).to.equal(invitationDaoStub);
expect(outbox._outboxBusy).to.be.false; expect(outbox._outboxBusy).to.be.false;
expect(outbox.pendingEmails).to.be.empty;
expect(emailDaoStub._account.folders[0].messages).to.equal(outbox.pendingEmails);
}); });
}); });
@ -50,50 +58,73 @@ define(function(require) {
}); });
describe('process outbox', function() { describe('process outbox', function() {
it('should work', function(done) { it('should send to registered users and update pending mails', function(done) {
var dummyMails = [{ var member, invited, notinvited, dummyMails, unsentCount;
member = {
id: '123', id: '123',
to: [{ to: [{
name: 'member', name: 'member',
address: 'member@whiteout.io' address: 'member@whiteout.io'
}] }]
}, { };
invited = {
id: '456', id: '456',
to: [{ to: [{
name: 'invited', name: 'invited',
address: 'invited@whiteout.io' address: 'invited@whiteout.io'
}] }]
}, { };
notinvited = {
id: '789', id: '789',
to: [{ to: [{
name: 'notinvited', name: 'notinvited',
address: 'notinvited@whiteout.io' address: 'notinvited@whiteout.io'
}] }]
}]; };
dummyMails = [member, invited, notinvited];
devicestorageStub.listItems.yieldsAsync(null, dummyMails); emailDaoStub.list.yieldsAsync(null, dummyMails);
emailDaoStub.encryptedSend.yieldsAsync(); emailDaoStub.sendEncrypted.withArgs(sinon.match(function(opts) {
emailDaoStub.send.yieldsAsync(); return typeof opts.email !== 'undefined' && opts.email.to.address === member.to.address;
})).yieldsAsync();
emailDaoStub.sendPlaintext.yieldsAsync();
devicestorageStub.removeList.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) {
invitationDaoStub.check.withArgs(sinon.match(function(o) { return o.recipient === 'notinvited@whiteout.io'; })).yieldsAsync(null, InvitationDAO.INVITE_MISSING); return o.recipient === 'invited@whiteout.io';
invitationDaoStub.invite.withArgs(sinon.match(function(o) { return o.recipient === 'notinvited@whiteout.io'; })).yieldsAsync(null, InvitationDAO.INVITE_SUCCESS); })).yieldsAsync(null, InvitationDAO.INVITE_PENDING);
keychainStub.getReceiverPublicKey.withArgs(sinon.match(function(o) { return o === 'member@whiteout.io'; })).yieldsAsync(null, 'this is not the key you are looking for...'); invitationDaoStub.check.withArgs(sinon.match(function(o) {
keychainStub.getReceiverPublicKey.withArgs(sinon.match(function(o) { return o === 'invited@whiteout.io' || o === 'notinvited@whiteout.io'; })).yieldsAsync(); 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() { var check = _.after(dummyMails.length + 1, function() {
expect(devicestorageStub.listItems.callCount).to.equal(1); expect(unsentCount).to.equal(2);
expect(emailDaoStub.encryptedSend.callCount).to.equal(1); expect(emailDaoStub.list.callCount).to.equal(1);
expect(emailDaoStub.send.callCount).to.equal(1); expect(emailDaoStub.sendEncrypted.callCount).to.equal(1);
expect(emailDaoStub.sendPlaintext.callCount).to.equal(1);
expect(devicestorageStub.removeList.callCount).to.equal(1); expect(devicestorageStub.removeList.callCount).to.equal(1);
expect(invitationDaoStub.check.callCount).to.equal(2); expect(invitationDaoStub.check.callCount).to.equal(2);
expect(invitationDaoStub.invite.callCount).to.equal(1); 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(); done();
}); });
function onOutboxUpdate(err, count) { function onOutboxUpdate(err, count) {
expect(err).to.not.exist; expect(err).to.not.exist;
expect(count).to.exist; expect(count).to.exist;
unsentCount = count;
check(); check();
} }

View File

@ -6,17 +6,17 @@ define(function(require) {
mocks = require('angularMocks'), mocks = require('angularMocks'),
WriteCtrl = require('js/controller/write'), WriteCtrl = require('js/controller/write'),
EmailDAO = require('js/dao/email-dao'), EmailDAO = require('js/dao/email-dao'),
DeviceStorageDAO = require('js/dao/devicestorage-dao'),
KeychainDAO = require('js/dao/keychain-dao'), KeychainDAO = require('js/dao/keychain-dao'),
appController = require('js/app-controller'); appController = require('js/app-controller');
describe('Write controller unit test', function() { describe('Write controller unit test', function() {
var ctrl, scope, origEmailDao, emailDaoMock, keychainMock, deviceStorageMock, emailAddress; var ctrl, scope, origEmailDao, emailDaoMock, keychainMock, emailAddress;
beforeEach(function() { beforeEach(function() {
origEmailDao = appController._emailDao; origEmailDao = appController._emailDao;
emailDaoMock = sinon.createStubInstance(EmailDAO); emailDaoMock = sinon.createStubInstance(EmailDAO);
appController._emailDao = emailDaoMock; appController._emailDao = emailDaoMock;
emailAddress = 'fred@foo.com'; emailAddress = 'fred@foo.com';
emailDaoMock._account = { emailDaoMock._account = {
emailAddress: emailAddress, emailAddress: emailAddress,
@ -25,9 +25,6 @@ define(function(require) {
keychainMock = sinon.createStubInstance(KeychainDAO); keychainMock = sinon.createStubInstance(KeychainDAO);
emailDaoMock._keychain = keychainMock; emailDaoMock._keychain = keychainMock;
deviceStorageMock = sinon.createStubInstance(DeviceStorageDAO);
emailDaoMock._devicestorage = deviceStorageMock;
angular.module('writetest', []); angular.module('writetest', []);
mocks.module('writetest'); mocks.module('writetest');
mocks.inject(function($rootScope, $controller) { mocks.inject(function($rootScope, $controller) {
@ -145,21 +142,37 @@ define(function(require) {
describe('send to outbox', function() { describe('send to outbox', function() {
it('should work', function(done) { it('should work', function(done) {
scope.state.writer.open = true; var verifyToSpy = sinon.spy(scope, 'verifyTo'),
scope.to = 'a, b, c'; re = {
scope.body = 'asd'; from: [{
scope.subject = 'yaddablabla'; address: 'pity@dafool'
scope.toKey = 'Public Key'; }],
subject: 'Ermahgerd!',
sentDate: new Date(),
body: 'so much body!'
};
deviceStorageMock.storeList.withArgs(sinon.match(function(mail) { scope.state.nav = {
return mail[0].from[0].address === emailAddress && mail[0].to.length === 3; currentFolder: 'currentFolder'
})).yieldsAsync(); };
scope.emptyOutbox = function() {
scope.emptyOutbox = function() {};
emailDaoMock.store.yields();
emailDaoMock.markAnswered.yields();
scope.onError = function(err) {
expect(err).to.not.exist;
expect(scope.state.writer.open).to.be.false; expect(scope.state.writer.open).to.be.false;
expect(deviceStorageMock.storeList.calledOnce).to.be.true; expect(emailDaoMock.store.calledOnce).to.be.true;
expect(emailDaoMock.store.calledOnce).to.be.true;
expect(verifyToSpy.calledOnce).to.be.true;
scope.verifyTo.restore();
done(); done();
}; };
scope.state.writer.write(re);
scope.sendToOutbox(); scope.sendToOutbox();
}); });
@ -170,8 +183,8 @@ define(function(require) {
scope.subject = 'yaddablabla'; scope.subject = 'yaddablabla';
scope.toKey = 'Public Key'; scope.toKey = 'Public Key';
deviceStorageMock.storeList.withArgs(sinon.match(function(mail) { emailDaoMock.store.withArgs(sinon.match(function(mail) {
return mail[0].from[0].address === emailAddress && mail[0].to.length === 3; return mail.from[0].address === emailAddress && mail.to.length === 3;
})).yields({ })).yields({
errMsg: 'snafu' errMsg: 'snafu'
}); });
@ -179,7 +192,7 @@ define(function(require) {
scope.onError = function(err) { scope.onError = function(err) {
expect(err).to.exist; expect(err).to.exist;
expect(scope.state.writer.open).to.be.true; expect(scope.state.writer.open).to.be.true;
expect(deviceStorageMock.storeList.calledOnce).to.be.true; expect(emailDaoMock.store.calledOnce).to.be.true;
done(); done();
}; };