mirror of
https://github.com/moparisthebest/mail
synced 2024-11-26 10:52:17 -05:00
[WO-57] Introduce encrypted outbox
The outbox is encrypted using the sender's keys. Prior to sending, every mail is buffered in the outbox.
This commit is contained in:
parent
0c6d279e82
commit
81a56a77c0
@ -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
|
||||||
@ -211,10 +211,10 @@ define(function(require) {
|
|||||||
smtpClient = new SmtpClient(smtpOptions);
|
smtpClient = new SmtpClient(smtpOptions);
|
||||||
pgp = new PGP();
|
pgp = new 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 = {
|
||||||
|
@ -12,10 +12,29 @@ 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(email, keychain, devicestorage, invitation) {
|
||||||
this._emailDao = emailDao;
|
/** @private */
|
||||||
this._invitationDao = invitationDao;
|
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;
|
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;
|
self._outboxBusy = true;
|
||||||
|
|
||||||
// get last item from outbox
|
// get last item from outbox
|
||||||
self._emailDao._devicestorage.listItems(dbType, 0, null, function(err, pending) {
|
self._email.list(function(err, pending) {
|
||||||
if (err) {
|
if (err) {
|
||||||
self._outboxBusy = false;
|
self._outboxBusy = false;
|
||||||
callback(err);
|
callback(err);
|
||||||
@ -70,6 +89,9 @@ define(function(require) {
|
|||||||
// update outbox folder count
|
// update outbox folder count
|
||||||
emails = pending;
|
emails = pending;
|
||||||
|
|
||||||
|
// keep an independent shallow copy of the pending mails array in the member
|
||||||
|
self.pendingEmails = pending.slice();
|
||||||
|
|
||||||
// sending pending mails
|
// sending pending mails
|
||||||
processMails();
|
processMails();
|
||||||
});
|
});
|
||||||
@ -80,12 +102,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 +129,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);
|
||||||
@ -125,7 +147,7 @@ define(function(require) {
|
|||||||
|
|
||||||
// invite the unregistered receivers, if necessary
|
// invite the unregistered receivers, if necessary
|
||||||
function invite(addresses) {
|
function invite(addresses) {
|
||||||
var sender = self._emailDao._account.emailAddress;
|
var sender = self._email._account.emailAddress;
|
||||||
|
|
||||||
var invitationFinished = _.after(addresses.length, function() {
|
var invitationFinished = _.after(addresses.length, function() {
|
||||||
// after all of the invitations are checked and sent (if necessary),
|
// after all of the invitations are checked and sent (if necessary),
|
||||||
@ -136,7 +158,7 @@ define(function(require) {
|
|||||||
addresses.forEach(function(recipient) {
|
addresses.forEach(function(recipient) {
|
||||||
var recipientAddress = recipient.address;
|
var recipientAddress = recipient.address;
|
||||||
|
|
||||||
self._invitationDao.check({
|
self._invitation.check({
|
||||||
recipient: recipientAddress,
|
recipient: recipientAddress,
|
||||||
sender: sender
|
sender: sender
|
||||||
}, function(err, status) {
|
}, function(err, status) {
|
||||||
@ -153,7 +175,7 @@ define(function(require) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// the recipient is not yet invited, so let's do that
|
// the recipient is not yet invited, so let's do that
|
||||||
self._invitationDao.invite({
|
self._invitation.invite({
|
||||||
recipient: recipientAddress,
|
recipient: recipientAddress,
|
||||||
sender: sender
|
sender: sender
|
||||||
}, function(err, status) {
|
}, function(err, status) {
|
||||||
@ -187,7 +209,7 @@ define(function(require) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// send invitation mail
|
// send invitation mail
|
||||||
self._emailDao.send(invitationMail, function(err) {
|
self._email.send(invitationMail, function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
self._outboxBusy = false;
|
self._outboxBusy = false;
|
||||||
callback(err);
|
callback(err);
|
||||||
@ -199,7 +221,8 @@ define(function(require) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sendEncrypted(email) {
|
function sendEncrypted(email) {
|
||||||
self._emailDao.encryptedSend(email, function(err) {
|
removeFromPendingMails(email);
|
||||||
|
self._email.encryptedSend(email, function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
self._outboxBusy = false;
|
self._outboxBusy = false;
|
||||||
callback(err);
|
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) {
|
function removeFromStorage(id) {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
self._outboxBusy = false;
|
self._outboxBusy = false;
|
||||||
@ -221,7 +250,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);
|
||||||
|
@ -7,7 +7,7 @@ define(function(require) {
|
|||||||
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,
|
var offset = 0,
|
||||||
@ -19,6 +19,8 @@ define(function(require) {
|
|||||||
//
|
//
|
||||||
|
|
||||||
emailDao = appController._emailDao;
|
emailDao = appController._emailDao;
|
||||||
|
outboxBo = appController._outboxBo;
|
||||||
|
|
||||||
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) {
|
||||||
@ -67,6 +69,13 @@ define(function(require) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
$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
|
// sync from imap to local db
|
||||||
syncImapFolder({
|
syncImapFolder({
|
||||||
@ -93,13 +102,26 @@ define(function(require) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var index, trashFolder;
|
var index, currentFolder, trashFolder, outboxFolder;
|
||||||
|
|
||||||
|
currentFolder = getFolder();
|
||||||
|
|
||||||
trashFolder = _.findWhere($scope.folders, {
|
trashFolder = _.findWhere($scope.folders, {
|
||||||
type: 'Trash'
|
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 = {
|
$scope.state.dialog = {
|
||||||
open: true,
|
open: true,
|
||||||
title: 'Delete',
|
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()) {
|
if (!getFolder()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// production... in chrome packaged app
|
|
||||||
if (window.chrome && chrome.identity) {
|
|
||||||
initList();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// development... display dummy mail objects
|
// development... display dummy mail objects
|
||||||
|
if (!window.chrome || !chrome.identity) {
|
||||||
firstSelect = true;
|
firstSelect = true;
|
||||||
updateStatus('Last update: ', new Date());
|
updateStatus('Last update: ', new Date());
|
||||||
$scope.emails = createDummyMails();
|
$scope.emails = createDummyMails();
|
||||||
$scope.select($scope.emails[0]);
|
$scope.select($scope.emails[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
// share local scope functions with root state
|
||||||
@ -196,23 +240,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) {
|
function syncImapFolder(options, callback) {
|
||||||
emailDao.unreadMessages(getFolder().path, function(err, unreadCount) {
|
emailDao.unreadMessages(getFolder().path, function(err, unreadCount) {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -270,14 +297,24 @@ define(function(require) {
|
|||||||
|
|
||||||
$scope.emails = emails;
|
$scope.emails = emails;
|
||||||
$scope.select($scope.emails[0]);
|
$scope.select($scope.emails[0]);
|
||||||
|
|
||||||
|
// 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();
|
$scope.$apply();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getFolder() {
|
function getFolder() {
|
||||||
return $scope.state.nav.currentFolder;
|
return $scope.state.nav.currentFolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
function markAsRead(email) {
|
function markAsRead(email) {
|
||||||
|
// marking mails as read is meaningless in the outbox
|
||||||
|
if (getFolder().type === 'Outbox') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// don't mark top selected email automatically
|
// don't mark top selected email automatically
|
||||||
if (firstSelect) {
|
if (firstSelect) {
|
||||||
firstSelect = false;
|
firstSelect = false;
|
||||||
|
@ -79,7 +79,7 @@ define(function(require) {
|
|||||||
// start checking outbox periodically
|
// start checking outbox periodically
|
||||||
outboxBo.startChecking($scope.onOutboxUpdate);
|
outboxBo.startChecking($scope.onOutboxUpdate);
|
||||||
// make function available globally for write controller
|
// make function available globally for write controller
|
||||||
$scope.emptyOutbox = outboxBo._processOutbox;
|
$scope.emptyOutbox = outboxBo._processOutbox.bind(outboxBo);
|
||||||
|
|
||||||
callback(folders);
|
callback(folders);
|
||||||
$scope.$apply();
|
$scope.$apply();
|
||||||
|
@ -165,9 +165,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;
|
||||||
|
@ -592,17 +592,27 @@ define(function(require) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// public key found... encrypt and send
|
// 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
|
* 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,
|
var self = this,
|
||||||
pt = email.body,
|
pt = options.email.body,
|
||||||
receiverPubkeys = [receiverPubkey];
|
receiverPubkeys = options.receiverPubkey ? [options.receiverPubkey] : [];
|
||||||
|
|
||||||
// get own public key so send message can be read
|
// get own public key so send message can be read
|
||||||
self._crypto.exportKeys(function(err, ownKeys) {
|
self._crypto.exportKeys(function(err, ownKeys) {
|
||||||
@ -621,9 +631,8 @@ define(function(require) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// bundle encrypted email together for sending
|
// bundle encrypted email together for sending
|
||||||
frameEncryptedMessage(email, ct);
|
frameEncryptedMessage(options.email, ct);
|
||||||
|
callback(null, options.email);
|
||||||
self.send(email, callback);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -631,7 +640,6 @@ define(function(require) {
|
|||||||
/**
|
/**
|
||||||
* Frames an encrypted message in base64 Format.
|
* Frames an encrypted message in base64 Format.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function frameEncryptedMessage(email, ct) {
|
function frameEncryptedMessage(email, ct) {
|
||||||
var to, greeting;
|
var to, greeting;
|
||||||
|
|
||||||
@ -645,8 +653,6 @@ define(function(require) {
|
|||||||
// build encrypted text body
|
// build encrypted text body
|
||||||
email.body = greeting + MESSAGE + ct + SIGNATURE;
|
email.body = greeting + MESSAGE + ct + SIGNATURE;
|
||||||
email.subject = str.subjectPrefix + email.subject;
|
email.subject = str.subjectPrefix + email.subject;
|
||||||
|
|
||||||
return email;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -658,5 +664,63 @@ define(function(require) {
|
|||||||
self._smtpClient.send(email, callback);
|
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;
|
return EmailDAO;
|
||||||
});
|
});
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
@ -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;
|
||||||
@ -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,
|
||||||
@ -134,7 +148,6 @@ define(function(require) {
|
|||||||
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);
|
expect(emailDaoMock.imapMarkMessageRead.callCount).to.equal(0);
|
||||||
done();
|
done();
|
||||||
};
|
};
|
||||||
@ -147,6 +160,8 @@ define(function(require) {
|
|||||||
it('should focus mail and mark it read', function() {
|
it('should focus mail and mark it read', function() {
|
||||||
var uid, mail, currentFolder;
|
var uid, mail, currentFolder;
|
||||||
|
|
||||||
|
scope._stopWatchTask();
|
||||||
|
|
||||||
uid = 123;
|
uid = 123;
|
||||||
mail = {
|
mail = {
|
||||||
uid: uid,
|
uid: uid,
|
||||||
@ -172,10 +187,62 @@ define(function(require) {
|
|||||||
notificationClickedHandler('123'); // first select, irrelevant
|
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);
|
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() {
|
describe('remove', function() {
|
||||||
it('should not delete without a selected mail', function() {
|
it('should not delete without a selected mail', function() {
|
||||||
scope.remove();
|
scope.remove();
|
||||||
@ -183,9 +250,36 @@ define(function(require) {
|
|||||||
expect(emailDaoMock.imapDeleteMessage.called).to.be.false;
|
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() {
|
it('should delete the selected mail from trash folder after clicking ok', function() {
|
||||||
var uid, mail, currentFolder;
|
var uid, mail, currentFolder;
|
||||||
|
|
||||||
|
scope._stopWatchTask();
|
||||||
|
|
||||||
uid = 123;
|
uid = 123;
|
||||||
mail = {
|
mail = {
|
||||||
uid: uid,
|
uid: uid,
|
||||||
@ -215,6 +309,8 @@ define(function(require) {
|
|||||||
it('should move the selected mail to the trash folder', function() {
|
it('should move the selected mail to the trash folder', function() {
|
||||||
var uid, mail, currentFolder, trashFolder;
|
var uid, mail, currentFolder, trashFolder;
|
||||||
|
|
||||||
|
scope._stopWatchTask();
|
||||||
|
|
||||||
uid = 123;
|
uid = 123;
|
||||||
mail = {
|
mail = {
|
||||||
uid: uid,
|
uid: uid,
|
||||||
|
@ -18,10 +18,10 @@ define(function(require) {
|
|||||||
emailDaoStub._account = {
|
emailDaoStub._account = {
|
||||||
emailAddress: dummyUser
|
emailAddress: dummyUser
|
||||||
};
|
};
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function() {});
|
afterEach(function() {});
|
||||||
@ -29,9 +29,12 @@ define(function(require) {
|
|||||||
describe('init', function() {
|
describe('init', function() {
|
||||||
it('should work', function() {
|
it('should work', function() {
|
||||||
expect(outbox).to.exist;
|
expect(outbox).to.exist;
|
||||||
expect(outbox._emailDao).to.equal(emailDaoStub);
|
expect(outbox._email).to.equal(emailDaoStub);
|
||||||
expect(outbox._invitationDao).to.equal(invitationDaoStub);
|
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._outboxBusy).to.be.false;
|
||||||
|
expect(outbox.pendingEmails).to.be.empty;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -50,50 +53,71 @@ 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.encryptedSend.yieldsAsync();
|
||||||
emailDaoStub.send.yieldsAsync();
|
emailDaoStub.send.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.list.callCount).to.equal(1);
|
||||||
expect(emailDaoStub.encryptedSend.callCount).to.equal(1);
|
expect(emailDaoStub.encryptedSend.callCount).to.equal(1);
|
||||||
expect(emailDaoStub.send.callCount).to.equal(1);
|
expect(emailDaoStub.send.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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
@ -174,12 +171,12 @@ 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;
|
||||||
})).yieldsAsync();
|
})).yieldsAsync();
|
||||||
scope.emptyOutbox = function() {
|
scope.emptyOutbox = function() {
|
||||||
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;
|
||||||
done();
|
done();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -193,8 +190,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'
|
||||||
});
|
});
|
||||||
@ -202,7 +199,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();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user