1
0
mirror of https://github.com/moparisthebest/mail synced 2025-02-07 02:20:14 -05:00

switching between offline and online state works

This commit is contained in:
Tankred Hase 2013-12-09 19:21:52 +01:00
parent f5b7b61e45
commit d08321d345
18 changed files with 1004 additions and 288 deletions

View File

@ -244,7 +244,7 @@ module.exports = function(grunt) {
// Test/Dev tasks
grunt.registerTask('dev', ['connect:dev']);
grunt.registerTask('test', ['jshint', 'connect:test', 'mocha', 'qunit']);
grunt.registerTask('test', ['jshint', 'connect:test', 'mocha']);
grunt.registerTask('prod', ['connect:prod']);
//

Binary file not shown.

View File

@ -18,4 +18,5 @@
<glyph unicode="&#xe008;" d="M0 960l1024-1024h-1024z" />
<glyph unicode="&#xe009;" d="M681.984 655.104v-119.744h-153.6v119.744c0 81.472-64.768 147.712-144.384 147.712s-144.384-66.24-144.384-147.712v-119.744h-153.6v119.744c-0.064 168.064 133.696 304.896 297.984 304.896 164.352 0 297.984-136.832 297.984-304.896zM0 476.096v-540.096h768v540.096h-768zM308.224-0.896l24.192 195.968 5.12 41.792c-21.632 15.232-35.712 40.64-35.712 69.44 0 11.136 2.112 21.696 5.888 31.296 12.096 30.848 41.664 52.736 76.288 52.736 34.624 0 64.192-21.888 76.288-52.736 3.776-9.664 5.888-20.224 5.888-31.296 0-28.8-14.208-54.272-35.84-69.312l5.184-41.856 24.064-195.968h-151.36z" horiz-adv-x="768" />
<glyph unicode="&#xe010;" d="M512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM512 32c-229.75 0-416 186.25-416 416s186.25 416 416 416 416-186.25 416-416-186.25-416-416-416zM448 704h128v-128h-128zM640 192h-256v64h64v192h-64v64h192v-256h64z" />
<glyph unicode="&#xe011;" d="M768 320.032l-182.82 182.822 438.82 329.15-128.010 127.996-548.52-219.442-172.7 172.706c-49.78 49.778-119.302 61.706-154.502 26.508-35.198-35.198-23.268-104.726 26.51-154.5l172.686-172.684-219.464-548.582 127.99-128.006 329.19 438.868 182.826-182.828v-255.98h127.994l63.992 191.988 191.988 63.996v127.992l-255.98-0.004z" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Binary file not shown.

View File

@ -24,7 +24,7 @@ define(function(require) {
/**
* Start the application
*/
self.start = function(callback) {
self.start = function(options, callback) {
// are we running in native app or in browser?
if (document.URL.indexOf("http") === 0 || document.URL.indexOf("app") === 0 || document.URL.indexOf("chrome") === 0) {
console.log('Assuming Browser environment...');
@ -36,12 +36,132 @@ define(function(require) {
function onDeviceReady() {
console.log('Starting app.');
// Handle offline and online gracefully
window.addEventListener('online', self.onConnect.bind(self, options.onError));
window.addEventListener('offline', self.onDisconnect.bind(self, options.onError));
// init app config storage
self._appConfigStore = new DeviceStorageDAO(new LawnchairDAO());
self._appConfigStore.init('app-config', callback);
}
};
self.onDisconnect = function(callback) {
if (!self._emailDao) {
// the following code only makes sense if the email dao has been initialized
return;
}
self._emailDao.onDisconnect(null, callback);
};
self.onConnect = function(callback) {
if (!self._emailDao) {
// the following code only makes sense if the email dao has been initialized
return;
}
if (!self.isOnline()) {
// prevent connection infinite loop
console.log('Not connecting since user agent is offline.');
callback();
return;
}
// fetch pinned local ssl certificate
self.getCertficate(function(err, certificate) {
if (err) {
callback(err);
return;
}
// get a fresh oauth token
self.fetchOAuthToken(function(err, oauth) {
if (err) {
callback(err);
return;
}
initClients(oauth, certificate);
});
});
function initClients(oauth, certificate) {
var auth, imapOptions, imapClient, smtpOptions, smtpClient;
auth = {
XOAuth2: {
user: oauth.emailAddress,
clientId: config.gmail.clientId,
accessToken: oauth.token
}
};
imapOptions = {
secure: config.gmail.imap.secure,
port: config.gmail.imap.port,
host: config.gmail.imap.host,
auth: auth,
ca: [certificate]
};
smtpOptions = {
secure: config.gmail.smtp.secure,
port: config.gmail.smtp.port,
host: config.gmail.smtp.host,
auth: auth,
ca: [certificate]
};
imapClient = new ImapClient(imapOptions);
smtpClient = new SmtpClient(smtpOptions);
imapClient.onError = function(err) {
console.log('IMAP error... reconnecting.', err);
// re-init client modules on error
self.onConnect(callback);
};
// connect to clients
self._emailDao.onConnect({
imapClient: imapClient,
smtpClient: smtpClient
}, callback);
}
};
self.getCertficate = function(localCallback) {
var xhr;
if (self.certificate) {
localCallback(null, self.certificate);
return;
}
// fetch pinned local ssl certificate
xhr = new XMLHttpRequest();
xhr.open('GET', '/ca/Google_Internet_Authority_G2.pem');
xhr.onload = function() {
if (xhr.readyState === 4 && xhr.status === 200 && xhr.responseText) {
self.certificate = xhr.responseText;
localCallback(null, self.certificate);
} else {
localCallback({
errMsg: 'Could not fetch pinned certificate!'
});
}
};
xhr.onerror = function() {
localCallback({
errMsg: 'Could not fetch pinned certificate!'
});
};
xhr.send();
};
self.isOnline = function() {
return navigator.onLine;
};
self.checkForUpdate = function() {
if (!chrome || !chrome.runtime || !chrome.runtime.onUpdateAvailable) {
return;
@ -63,6 +183,121 @@ define(function(require) {
});
};
/**
* Gracefully try to fetch the user's email address from local storage.
* If not yet stored, handle online/offline cases on first use.
*/
self.getEmailAddress = function(callback) {
// try to fetch email address from local storage
self.getEmailAddressFromConfig(function(err, cachedEmailAddress) {
if (err) {
callback(err);
return;
}
if (cachedEmailAddress) {
// not first time login... address cached
callback(null, cachedEmailAddress);
return;
}
if (!cachedEmailAddress && !self.isOnline()) {
// first time login... must be online
callback({
errMsg: 'The app must be online on first use!'
});
return;
}
self.fetchOAuthToken(function(err, oauth) {
if (err) {
callback(err);
return;
}
callback(null, oauth.emailAddress);
});
});
};
/**
* Get the user's email address from local storage
*/
self.getEmailAddressFromConfig = function(callback) {
self._appConfigStore.listItems('emailaddress', 0, null, function(err, cachedItems) {
if (err) {
callback(err);
return;
}
// no email address is cached yet
if (!cachedItems || cachedItems.length < 1) {
callback();
return;
}
callback(null, cachedItems[0]);
});
};
/**
* Lookup the user's email address. Check local cache if available
* otherwise query google's token info api to learn the user's email address
*/
self.queryEmailAddress = function(token, callback) {
var itemKey = 'emailaddress';
self.getEmailAddressFromConfig(function(err, cachedEmailAddress) {
if (err) {
callback(err);
return;
}
// do roundtrip to google api if no email address is cached yet
if (!cachedEmailAddress) {
queryGoogleApi();
return;
}
callback(null, cachedEmailAddress);
});
function queryGoogleApi() {
if (!token) {
callback({
errMsg: 'Invalid OAuth token!'
});
return;
}
// fetch gmail user's email address from the Google Authorization Server endpoint
$.ajax({
url: 'https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=' + token,
type: 'GET',
dataType: 'json',
success: function(info) {
if (!info || !info.email) {
callback({
errMsg: 'Error looking up email address on google api!'
});
return;
}
// cache the email address on the device
self._appConfigStore.storeList([info.email], itemKey, function(err) {
callback(err, info.email);
});
},
error: function(xhr, textStatus, err) {
callback({
errMsg: xhr.status + ': ' + xhr.statusText,
err: err
});
}
});
}
};
/**
* Request an OAuth token from chrome for gmail users
*/
@ -100,138 +335,59 @@ define(function(require) {
);
};
/**
* Lookup the user's email address. Check local cache if available, otherwise query google's token info api to learn the user's email address
*/
self.queryEmailAddress = function(token, callback) {
var itemKey = 'emailaddress';
self.buildModules = function() {
var lawnchairDao, restDao, pubkeyDao, invitationDao,
emailDao, keychain, pgp, userStorage;
self._appConfigStore.listItems(itemKey, 0, null, function(err, cachedItems) {
if (err) {
callback(err);
return;
}
// init objects and inject dependencies
restDao = new RestDAO();
pubkeyDao = new PublicKeyDAO(restDao);
invitationDao = new InvitationDAO(restDao);
lawnchairDao = new LawnchairDAO();
userStorage = new DeviceStorageDAO(lawnchairDao);
// do roundtrip to google api if no email address is cached yet
if (!cachedItems || cachedItems.length < 1) {
queryGoogleApi();
return;
}
callback(null, cachedItems[0]);
});
function queryGoogleApi() {
// fetch gmail user's email address from the Google Authorization Server endpoint
$.ajax({
url: 'https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=' + token,
type: 'GET',
dataType: 'json',
success: function(info) {
if (!info || !info.email) {
callback({
errMsg: 'Error looking up email address on google api!'
});
return;
}
// cache the email address on the device
self._appConfigStore.storeList([info.email], itemKey, function(err) {
callback(err, info.email);
});
},
error: function(xhr, textStatus, err) {
callback({
errMsg: xhr.status + ': ' + xhr.statusText,
err: err
});
}
});
}
keychain = new KeychainDAO(lawnchairDao, pubkeyDao);
self._keychain = keychain;
pgp = new PGP();
self._crypto = pgp;
self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage);
self._outboxBo = new OutboxBO(emailDao, keychain, userStorage, invitationDao);
};
/**
* Instanciate the mail email data access object and its dependencies. Login to imap on init.
*/
self.init = function(userId, token, callback) {
var auth, imapOptions, smtpOptions, certificate,
lawnchairDao, restDao, pubkeyDao, invitationDao, emailDao,
keychain, imapClient, smtpClient, pgp, userStorage, xhr;
self.init = function(options, callback) {
self.buildModules();
// fetch pinned local ssl certificate
xhr = new XMLHttpRequest();
xhr.open('GET', '/ca/Google_Internet_Authority_G2.pem');
xhr.onload = function() {
if (xhr.readyState === 4 && xhr.status === 200 && xhr.responseText) {
certificate = xhr.responseText;
setupDaos();
} else {
callback({
errMsg: 'Could not fetch pinned certificate!'
});
// init email dao
var account = {
emailAddress: options.emailAddress,
asymKeySize: config.asymKeySize
};
self._emailDao.init({
account: account
}, function(err, keypair) {
// dont handle offline case this time
if (err && err.code !== 42) {
callback(err);
return;
}
};
xhr.onerror = function() {
callback({
errMsg: 'Could not fetch pinned certificate!'
});
};
xhr.send();
function setupDaos() {
// create mail credentials objects for imap/smtp
auth = {
XOAuth2: {
user: userId,
clientId: config.gmail.clientId,
accessToken: token
// connect tcp clients on first startup
self.onConnect(function(err) {
if (err) {
callback(err);
return;
}
};
imapOptions = {
secure: config.gmail.imap.secure,
port: config.gmail.imap.port,
host: config.gmail.imap.host,
auth: auth,
ca: [certificate]
};
smtpOptions = {
secure: config.gmail.smtp.secure,
port: config.gmail.smtp.port,
host: config.gmail.smtp.host,
auth: auth,
ca: [certificate]
};
// init objects and inject dependencies
restDao = new RestDAO();
pubkeyDao = new PublicKeyDAO(restDao);
lawnchairDao = new LawnchairDAO();
keychain = new KeychainDAO(lawnchairDao, pubkeyDao);
self._keychain = keychain;
imapClient = new ImapClient(imapOptions);
smtpClient = new SmtpClient(smtpOptions);
pgp = new PGP();
self._crypto = pgp;
userStorage = new DeviceStorageDAO(lawnchairDao);
self._emailDao = emailDao = new EmailDAO(keychain, imapClient, smtpClient, pgp, userStorage);
invitationDao = new InvitationDAO(restDao);
self._outboxBo = new OutboxBO(emailDao, keychain, userStorage, invitationDao);
// init email dao
var account = {
emailAddress: userId,
asymKeySize: config.asymKeySize
};
self._emailDao.init({
account: account
}, function(err, keypair) {
// init outbox
self._outboxBo.init();
callback(err, keypair);
});
}
callback(null, keypair);
});
});
};
return self;

View File

@ -232,7 +232,12 @@ define(function(require) {
self._emailDao.sendPlaintext(invitationMail, function(err) {
if (err) {
self._outboxBusy = false;
callback(err);
if (err.code === 42) {
// offline try again later
callback();
} else {
callback(err);
}
return;
}
@ -251,7 +256,12 @@ define(function(require) {
}, function(err) {
if (err) {
self._outboxBusy = false;
callback(err);
if (err.code === 42) {
// offline try again later
callback();
} else {
callback(err);
}
return;
}

View File

@ -14,32 +14,29 @@ define(function(require) {
appController.checkForUpdate();
// start main application controller
appController.start(function(err) {
appController.start({
onError: $scope.onError
}, function(err) {
if (err) {
$scope.onError(err);
return;
}
if (!window.chrome || !chrome.identity) {
$location.path('/desktop');
$scope.$apply();
return;
}
// login to imap
initializeUser();
});
function initializeUser() {
// get OAuth token from chrome
appController.fetchOAuthToken(function(err, auth) {
appController.getEmailAddress(function(err, emailAddress) {
if (err) {
$scope.onError(err);
return;
}
// initiate controller by creating email dao
appController.init(auth.emailAddress, auth.token, function(err, availableKeys) {
appController.init({
emailAddress: emailAddress
}, function(err, availableKeys) {
if (err) {
$scope.onError(err);
return;

View File

@ -91,6 +91,13 @@ define(function(require) {
return;
}
if (err && err.code === 42) {
// offline
updateStatus('Offline mode');
$scope.$apply();
return;
}
if (err) {
updateStatus('Error on sync!');
$scope.onError(err);
@ -150,6 +157,19 @@ define(function(require) {
}
};
// share local scope functions with root state
$scope.state.mailList = {
remove: $scope.remove,
synchronize: $scope.synchronize
};
//
// watch tasks
//
/**
* List emails from folder when user changes folder
*/
$scope._stopWatchTask = $scope.$watch('state.nav.currentFolder', function() {
if (!getFolder()) {
return;
@ -177,11 +197,16 @@ define(function(require) {
$scope.synchronize();
});
// share local scope functions with root state
$scope.state.mailList = {
remove: $scope.remove,
synchronize: $scope.synchronize
};
/**
* Sync current folder when client comes back online
*/
$scope.$watch('account.online', function(isOnline) {
if (isOnline) {
$scope.synchronize();
} else {
updateStatus('Offline mode');
}
}, true);
//
// helper functions

View File

@ -181,7 +181,15 @@ define(function(require) {
$scope.replyTo.answered = true;
emailDao.sync({
folder: $scope.state.nav.currentFolder.path
}, $scope.onError);
}, function(err) {
if (err && err.code === 42) {
// offline
$scope.onError();
return;
}
$scope.onError(err);
});
}
};

View File

@ -6,21 +6,12 @@ define(function(require) {
str = require('js/app-config').string,
config = require('js/app-config').config;
var EmailDAO = function(keychain, imapClient, smtpClient, crypto, devicestorage) {
var EmailDAO = function(keychain, crypto, devicestorage) {
var self = this;
self._keychain = keychain;
self._imapClient = imapClient;
self._smtpClient = smtpClient;
self._crypto = crypto;
self._devicestorage = devicestorage;
// delegation-esque pattern to mitigate between node-style events and plain js
self._imapClient.onIncomingMessage = function(message) {
if (typeof self.onIncomingMessage === 'function') {
self.onIncomingMessage(message);
}
};
};
//
@ -33,6 +24,7 @@ define(function(require) {
self._account = options.account;
self._account.busy = false;
self._account.online = false;
// validate email address
var emailAddress = self._account.emailAddress;
@ -63,31 +55,70 @@ define(function(require) {
}
function initFolders() {
self._imapLogin(function(err) {
// try init folders from memory, since imap client not initiated yet
self._imapListFolders(function(err, folders) {
if (err) {
callback(err);
return;
}
self._imapListFolders(function(err, folders) {
if (err) {
callback(err);
return;
}
// every folder is initially created with an unread count of 0.
// the unread count will be updated after every sync
folders.forEach(function(folder){
folder.count = 0;
});
self._account.folders = folders;
callback(null, keypair);
});
self._account.folders = folders;
callback(null, keypair);
});
}
};
EmailDAO.prototype.onConnect = function(options, callback) {
var self = this;
self._imapClient = options.imapClient;
self._smtpClient = options.smtpClient;
// delegation-esque pattern to mitigate between node-style events and plain js
self._imapClient.onIncomingMessage = function(message) {
if (typeof self.onIncomingMessage === 'function') {
self.onIncomingMessage(message);
}
};
// connect to newly created imap client
self._imapLogin(function(err) {
if (err) {
callback(err);
return;
}
// set status to online
self._account.online = true;
// check memory
if (self._account.folders) {
// no need to init folder again on connect... already in memory
callback();
return;
}
// init folders
self._imapListFolders(function(err, folders) {
if (err) {
callback(err);
return;
}
self._account.folders = folders;
callback();
});
});
};
EmailDAO.prototype.onDisconnect = function(options, callback) {
// set status to online
this._account.online = false;
this._imapClient = undefined;
this._smtpClient = undefined;
callback();
};
EmailDAO.prototype.unlock = function(options, callback) {
var self = this;
@ -180,6 +211,7 @@ define(function(require) {
return;
}
// check busy status
if (self._account.busy) {
callback({
errMsg: 'Sync aborted: Previous sync still in progress',
@ -188,6 +220,7 @@ define(function(require) {
return;
}
// not busy -> set busy
self._account.busy = true;
folder = _.findWhere(self._account.folders, {
@ -547,7 +580,6 @@ define(function(require) {
});
});
}
});
}
@ -705,6 +737,14 @@ define(function(require) {
};
EmailDAO.prototype._imapMark = function(options, callback) {
if (!this._imapClient) {
callback({
errMsg: 'Client is currently offline!',
code: 42
});
return;
}
this._imapClient.updateFlags({
path: options.folder,
uid: options.uid,
@ -714,6 +754,14 @@ define(function(require) {
};
EmailDAO.prototype.move = function(options, callback) {
if (!this._imapClient) {
callback({
errMsg: 'Client is currently offline!',
code: 42
});
return;
}
this._imapClient.moveMessage({
path: options.folder,
uid: options.uid,
@ -725,6 +773,14 @@ define(function(require) {
var self = this,
email = options.email;
if (!self._smtpClient) {
callback({
errMsg: 'Client is currently offline!',
code: 42
});
return;
}
// validate the email input
if (!email.to || !email.from || !email.to[0].address || !email.from[0].address) {
callback({
@ -782,6 +838,14 @@ define(function(require) {
};
EmailDAO.prototype.sendPlaintext = function(options, callback) {
if (!this._smtpClient) {
callback({
errMsg: 'Client is currently offline!',
code: 42
});
return;
}
this._smtpClient.send(options.email, callback);
};
@ -866,6 +930,14 @@ define(function(require) {
* Login the imap client
*/
EmailDAO.prototype._imapLogin = function(callback) {
if (!this._imapClient) {
callback({
errMsg: 'Client is currently offline!',
code: 42
});
return;
}
// login IMAP client if existent
this._imapClient.login(callback);
};
@ -874,6 +946,14 @@ define(function(require) {
* Cleanup by logging the user off.
*/
EmailDAO.prototype._imapLogout = function(callback) {
if (!this._imapClient) {
callback({
errMsg: 'Client is currently offline!',
code: 42
});
return;
}
this._imapClient.logout(callback);
};
@ -882,6 +962,14 @@ define(function(require) {
* @param {String} options.folderName The name of the imap folder.
*/
EmailDAO.prototype._imapListMessages = function(options, callback) {
if (!this._imapClient) {
callback({
errMsg: 'Client is currently offline!',
code: 42
});
return;
}
this._imapClient.listMessages({
path: options.folder,
offset: 0,
@ -890,6 +978,14 @@ define(function(require) {
};
EmailDAO.prototype._imapDeleteMessage = function(options, callback) {
if (!this._imapClient) {
callback({
errMsg: 'Client is currently offline!',
code: 42
});
return;
}
this._imapClient.deleteMessage({
path: options.folder,
uid: options.uid
@ -901,6 +997,14 @@ define(function(require) {
* @param {String} options.messageId The
*/
EmailDAO.prototype._imapGetMessage = function(options, callback) {
if (!this._imapClient) {
callback({
errMsg: 'Client is currently offline!',
code: 42
});
return;
}
this._imapClient.getMessagePreview({
path: options.folder,
uid: options.uid
@ -933,6 +1037,14 @@ define(function(require) {
function fetchFromServer() {
var folders;
if (!self._imapClient) {
callback({
errMsg: 'Client is currently offline!',
code: 42
});
return;
}
// fetch list from imap server
self._imapClient.listWellKnownFolders(function(err, wellKnownFolders) {
if (err) {

View File

@ -63,6 +63,12 @@
color: $color-grey-dark;
line-height: em(28,12);
.offline {
&[data-icon]:before {
padding-right: 0.5em;
}
}
&.syncing {
.spinner {
top: 6.5px;

View File

@ -23,6 +23,11 @@
<footer ng-class="{syncing: account.busy}" ng-click="synchronize()">
<span class="spinner"></span>
<span class="text">{{lastUpdateLbl}} {{lastUpdate | date:'shortTime'}}</span>
<span class="text" ng-switch="account.online">
<span ng-switch-when="false">
<span class="offline" data-icon="&#xe011;"></span>
</span>
{{lastUpdateLbl}} {{lastUpdate | date:'shortTime'}}
</span>
</footer>
</div><!--/.view-mail-list-->

View File

@ -4,18 +4,19 @@
</header>
<ul class="nav-primary">
<li ng-repeat="folder in account.folders" ng-switch="folder.count !== undefined">
<li ng-repeat="folder in account.folders" ng-switch="folder.count !== undefined && folder.count > 0">
<a href="#" ng-click="openFolder(folder); $event.preventDefault()">
{{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>
</a>
</li>
</ul>
<ul class="nav-secondary">
<li><a href="#" ng-click="state.account.toggle(true); $event.preventDefault()">Account</a></li>
<li><a href="#" ng-click="$event.preventDefault()">About whiteout.io</a></li>
<li><a href="#" ng-click="$event.preventDefault()">Help</a></li>
<li><a href="http://whiteout.io" target="_blank">About whiteout.io</a></li>
</ul>
<footer>

View File

@ -3,6 +3,7 @@ define(function(require) {
var controller = require('js/app-controller'),
EmailDAO = require('js/dao/email-dao'),
OutboxBO = require('js/bo/outbox'),
DeviceStorageDAO = require('js/dao/devicestorage-dao'),
$ = require('jquery'),
expect = chai.expect;
@ -13,12 +14,18 @@ define(function(require) {
};
describe('App Controller unit tests', function() {
var emailDaoStub, outboxStub, appConfigStoreStub, isOnlineStub,
identityStub;
beforeEach(function() {
sinon.stub(controller, 'init', function(userId, password, token, callback) {
controller._emailDao = sinon.createStubInstance(EmailDAO);
callback();
});
emailDaoStub = sinon.createStubInstance(EmailDAO);
controller._emailDao = emailDaoStub;
outboxStub = sinon.createStubInstance(OutboxBO);
controller._outboxBo = outboxStub;
appConfigStoreStub = sinon.createStubInstance(DeviceStorageDAO);
controller._appConfigStore = appConfigStoreStub;
isOnlineStub = sinon.stub(controller, 'isOnline');
sinon.stub($, 'get');
sinon.stub($, 'ajax').yieldsTo('success', {
@ -30,58 +37,285 @@ define(function(require) {
if (typeof window.chrome.identity.getAuthToken !== 'function') {
window.chrome.identity.getAuthToken = function() {};
}
sinon.stub(window.chrome.identity, 'getAuthToken');
window.chrome.identity.getAuthToken.yields('token42');
identityStub = sinon.stub(window.chrome.identity, 'getAuthToken');
});
afterEach(function() {
controller.init.restore();
$.get.restore();
$.ajax.restore();
window.chrome.identity.getAuthToken.restore();
identityStub.restore();
isOnlineStub.restore();
});
describe('start', function() {
it('should not explode', function(done) {
controller.start(function(err) {
controller.start({
onError: function() {}
}, function(err) {
expect(err).to.not.exist;
done();
});
});
});
describe('onDisconnect', function() {
it('should work', function(done) {
emailDaoStub.onDisconnect.yields();
controller.onDisconnect(function(err) {
expect(err).to.not.exist;
done();
});
});
});
describe('onConnect', function() {
var fetchOAuthTokenStub, getCertficateStub;
beforeEach(function() {
// buildModules
fetchOAuthTokenStub = sinon.stub(controller, 'fetchOAuthToken');
getCertficateStub = sinon.stub(controller, 'getCertficate');
});
afterEach(function() {
fetchOAuthTokenStub.restore();
getCertficateStub.restore();
});
it('should not connect if offline', function(done) {
isOnlineStub.returns(false);
controller.onConnect(function(err) {
expect(err).to.not.exist;
done();
});
});
it('should fail due to error in certificate', function(done) {
isOnlineStub.returns(true);
getCertficateStub.yields({});
controller.onConnect(function(err) {
expect(err).to.exist;
expect(getCertficateStub.calledOnce).to.be.true;
done();
});
});
it('should fail due to error in fetch oauth', function(done) {
isOnlineStub.returns(true);
getCertficateStub.yields(null, 'PEM');
fetchOAuthTokenStub.yields({});
controller.onConnect(function(err) {
expect(err).to.exist;
expect(fetchOAuthTokenStub.calledOnce).to.be.true;
expect(getCertficateStub.calledOnce).to.be.true;
done();
});
});
it('should work', function(done) {
isOnlineStub.returns(true);
fetchOAuthTokenStub.yields(null, {
emailAddress: 'asfd@example.com'
});
getCertficateStub.yields(null, 'PEM');
emailDaoStub.onConnect.yields();
controller.onConnect(function(err) {
expect(err).to.not.exist;
expect(fetchOAuthTokenStub.calledOnce).to.be.true;
expect(getCertficateStub.calledOnce).to.be.true;
expect(emailDaoStub.onConnect.calledOnce).to.be.true;
done();
});
});
});
describe('getEmailAddress', function() {
var fetchOAuthTokenStub;
beforeEach(function() {
// buildModules
fetchOAuthTokenStub = sinon.stub(controller, 'fetchOAuthToken');
});
afterEach(function() {
fetchOAuthTokenStub.restore();
});
it('should fail due to error in config list items', function(done) {
appConfigStoreStub.listItems.yields({});
controller.getEmailAddress(function(err, emailAddress) {
expect(err).to.exist;
expect(emailAddress).to.not.exist;
done();
});
});
it('should work if address is already cached', function(done) {
appConfigStoreStub.listItems.yields(null, ['asdf']);
controller.getEmailAddress(function(err, emailAddress) {
expect(err).to.not.exist;
expect(emailAddress).to.exist;
done();
});
});
it('should fail first time if app is offline', function(done) {
appConfigStoreStub.listItems.yields(null, []);
isOnlineStub.returns(false);
controller.getEmailAddress(function(err, emailAddress) {
expect(err).to.exist;
expect(emailAddress).to.not.exist;
expect(isOnlineStub.calledOnce).to.be.true;
done();
});
});
it('should fail due to error in fetchOAuthToken', function(done) {
appConfigStoreStub.listItems.yields(null, []);
isOnlineStub.returns(true);
fetchOAuthTokenStub.yields({});
controller.getEmailAddress(function(err, emailAddress) {
expect(err).to.exist;
expect(emailAddress).to.not.exist;
expect(fetchOAuthTokenStub.calledOnce).to.be.true;
done();
});
});
it('should fail work when fetching oauth token', function(done) {
appConfigStoreStub.listItems.yields(null, []);
isOnlineStub.returns(true);
fetchOAuthTokenStub.yields(null, {
emailAddress: 'asfd@example.com'
});
controller.getEmailAddress(function(err, emailAddress) {
expect(err).to.not.exist;
expect(emailAddress).to.equal('asfd@example.com');
expect(fetchOAuthTokenStub.calledOnce).to.be.true;
done();
});
});
});
describe('fetchOAuthToken', function() {
beforeEach(function() {
controller._appConfigStore = sinon.createStubInstance(DeviceStorageDAO);
});
it('should work the first time', function(done) {
controller._appConfigStore.listItems.yields(null, []);
controller._appConfigStore.storeList.yields();
appConfigStoreStub.listItems.yields(null, []);
appConfigStoreStub.storeList.yields();
identityStub.yields('token42');
controller.fetchOAuthToken(function(err) {
expect(err).to.not.exist;
expect(controller._appConfigStore.listItems.calledOnce).to.be.true;
expect(controller._appConfigStore.storeList.calledOnce).to.be.true;
expect(window.chrome.identity.getAuthToken.calledOnce).to.be.true;
expect(appConfigStoreStub.listItems.calledOnce).to.be.true;
expect(appConfigStoreStub.storeList.calledOnce).to.be.true;
expect(identityStub.calledOnce).to.be.true;
expect($.ajax.calledOnce).to.be.true;
done();
});
});
it('should work when the email address is cached', function(done) {
controller._appConfigStore.listItems.yields(null, ['asdf']);
appConfigStoreStub.listItems.yields(null, ['asdf']);
identityStub.yields('token42');
controller.fetchOAuthToken(function(err) {
expect(err).to.not.exist;
expect(controller._appConfigStore.listItems.calledOnce).to.be.true;
expect(window.chrome.identity.getAuthToken.calledOnce).to.be.true;
expect(appConfigStoreStub.listItems.calledOnce).to.be.true;
expect(identityStub.calledOnce).to.be.true;
expect($.ajax.called).to.be.false;
done();
});
});
});
describe('buildModules', function() {
it('should work', function() {
controller.buildModules();
expect(controller._keychain).to.exist;
expect(controller._crypto).to.exist;
expect(controller._emailDao).to.exist;
expect(controller._outboxBo).to.exist;
});
});
describe('init', function() {
var buildModulesStub, onConnectStub;
beforeEach(function() {
// buildModules
buildModulesStub = sinon.stub(controller, 'buildModules');
buildModulesStub.returns();
// onConnect
onConnectStub = sinon.stub(controller, 'onConnect');
});
afterEach(function() {
buildModulesStub.restore();
onConnectStub.restore();
});
it('should fail due to error in emailDao.init', function(done) {
emailDaoStub.init.yields({});
controller.init({}, function(err) {
expect(err).to.exist;
done();
});
});
it('should fail due to error in onConnect', function(done) {
emailDaoStub.init.yields();
onConnectStub.yields({});
controller.init({}, function(err) {
expect(err).to.exist;
expect(onConnectStub.calledOnce).to.be.true;
done();
});
});
it('should pass email dao init when offline', function(done) {
emailDaoStub.init.yields({
code: 42
});
onConnectStub.yields();
outboxStub.init.returns();
controller.init({}, function(err) {
expect(err).to.not.exist;
expect(onConnectStub.calledOnce).to.be.true;
expect(outboxStub.init.calledOnce).to.be.true;
done();
});
});
it('should work and return a keypair', function(done) {
emailDaoStub.init.yields(null, {});
onConnectStub.yields();
outboxStub.init.returns();
controller.init({}, function(err, keypair) {
expect(err).to.not.exist;
expect(keypair).to.exist;
expect(onConnectStub.calledOnce).to.be.true;
expect(outboxStub.init.calledOnce).to.be.true;
done();
});
});
});
});
});

View File

@ -17,7 +17,7 @@ define(function(require) {
dummyDecryptedMail, mockKeyPair, account, publicKey, verificationMail, verificationUuid,
nonWhitelistedMail;
beforeEach(function() {
beforeEach(function(done) {
emailAddress = 'asdf@asdf.com';
passphrase = 'asdf';
asymKeySize = 2048;
@ -98,17 +98,41 @@ define(function(require) {
pgpStub = sinon.createStubInstance(PGP);
devicestorageStub = sinon.createStubInstance(DeviceStorageDAO);
dao = new EmailDAO(keychainStub, imapClientStub, smtpClientStub, pgpStub, devicestorageStub);
dao = new EmailDAO(keychainStub, pgpStub, devicestorageStub);
dao._account = account;
expect(dao._keychain).to.equal(keychainStub);
expect(dao._imapClient).to.equal(imapClientStub);
expect(dao._smtpClient).to.equal(smtpClientStub);
expect(dao._crypto).to.equal(pgpStub);
expect(dao._devicestorage).to.equal(devicestorageStub);
// connect
expect(dao._imapClient).to.not.exist;
expect(dao._smtpClient).to.not.exist;
expect(dao._account.online).to.be.undefined;
dao._account.folders = [];
imapClientStub.login.yields();
dao.onConnect({
imapClient: imapClientStub,
smtpClient: smtpClientStub
}, function(err) {
expect(err).to.not.exist;
expect(dao._account.online).to.be.true;
expect(dao._imapClient).to.equal(dao._imapClient);
expect(dao._smtpClient).to.equal(dao._smtpClient);
done();
});
});
afterEach(function() {});
afterEach(function(done) {
dao.onDisconnect(null, function(err) {
expect(err).to.not.exist;
expect(dao._account.online).to.be.false;
expect(dao._imapClient).to.not.exist;
expect(dao._smtpClient).to.not.exist;
done();
});
});
describe('push', function() {
it('should work', function(done) {
@ -129,7 +153,7 @@ define(function(require) {
});
it('should init', function(done) {
var loginStub, listFolderStub, folders;
var listFolderStub, folders;
folders = [{}, {}];
@ -138,24 +162,22 @@ define(function(require) {
keychainStub.getUserKeyPair.yields(null, mockKeyPair);
// initFolders
loginStub = sinon.stub(dao, '_imapLogin');
listFolderStub = sinon.stub(dao, '_imapListFolders');
loginStub.yields();
listFolderStub.yields(null, folders);
dao.init({
account: account
}, function(err, keyPair) {
expect(err).to.not.exist;
expect(dao._account.busy).to.be.false;
expect(dao._account.online).to.be.false;
expect(keyPair).to.equal(mockKeyPair);
expect(dao._account).to.equal(account);
expect(dao._account.folders).to.equal(folders);
expect(dao._account.folders[0].count).to.equal(0);
expect(devicestorageStub.init.calledOnce).to.be.true;
expect(keychainStub.getUserKeyPair.calledOnce).to.be.true;
expect(loginStub.calledOnce).to.be.true;
expect(listFolderStub.calledOnce).to.be.true;
done();
@ -163,16 +185,14 @@ define(function(require) {
});
it('should fail due to error while listing folders', function(done) {
var loginStub, listFolderStub;
var listFolderStub;
// initKeychain
devicestorageStub.init.withArgs(emailAddress).yields();
keychainStub.getUserKeyPair.yields(null, mockKeyPair);
// initFolders
loginStub = sinon.stub(dao, '_imapLogin');
listFolderStub = sinon.stub(dao, '_imapListFolders');
loginStub.yields();
listFolderStub.yields({});
dao.init({
@ -184,40 +204,12 @@ define(function(require) {
expect(dao._account).to.equal(account);
expect(devicestorageStub.init.calledOnce).to.be.true;
expect(keychainStub.getUserKeyPair.calledOnce).to.be.true;
expect(loginStub.calledOnce).to.be.true;
expect(listFolderStub.calledOnce).to.be.true;
done();
});
});
it('should fail due to error during imap login', function(done) {
var loginStub = sinon.stub(dao, '_imapLogin');
// initKeychain
devicestorageStub.init.withArgs(emailAddress).yields();
keychainStub.getUserKeyPair.yields(null, mockKeyPair);
// initFolders
loginStub.yields({});
dao.init({
account: account
}, function(err, keyPair) {
expect(err).to.exist;
expect(keyPair).to.not.exist;
expect(dao._account).to.equal(account);
expect(devicestorageStub.init.calledOnce).to.be.true;
expect(keychainStub.getUserKeyPair.calledOnce).to.be.true;
expect(loginStub.calledOnce).to.be.true;
done();
});
});
it('should fail due to error in getUserKeyPair', function(done) {
devicestorageStub.init.yields();
keychainStub.getUserKeyPair.yields({});
@ -235,6 +227,77 @@ define(function(require) {
});
});
describe('onConnect', function() {
var imapLoginStub, imapListFoldersStub;
beforeEach(function(done) {
// imap login
imapLoginStub = sinon.stub(dao, '_imapLogin');
imapListFoldersStub = sinon.stub(dao, '_imapListFolders');
dao.onDisconnect(null, function(err) {
expect(err).to.not.exist;
expect(dao._imapClient).to.not.exist;
expect(dao._smtpClient).to.not.exist;
expect(dao._account.online).to.be.false;
done();
});
});
afterEach(function() {
imapLoginStub.restore();
imapListFoldersStub.restore();
});
it('should fail due to error in imap login', function(done) {
imapLoginStub.yields({});
dao.onConnect({
imapClient: imapClientStub,
smtpClient: smtpClientStub
}, function(err) {
expect(err).to.exist;
expect(imapLoginStub.calledOnce).to.be.true;
expect(dao._account.online).to.be.false;
done();
});
});
it('should work when folder already initiated', function(done) {
dao._account.folders = [];
imapLoginStub.yields();
dao.onConnect({
imapClient: imapClientStub,
smtpClient: smtpClientStub
}, function(err) {
expect(err).to.not.exist;
expect(dao._account.online).to.be.true;
expect(dao._imapClient).to.equal(dao._imapClient);
expect(dao._smtpClient).to.equal(dao._smtpClient);
done();
});
});
it('should work when folder not yet initiated', function(done) {
var folders = [];
imapLoginStub.yields();
imapListFoldersStub.yields(null, folders);
dao.onConnect({
imapClient: imapClientStub,
smtpClient: smtpClientStub
}, function(err) {
expect(err).to.not.exist;
expect(dao._account.online).to.be.true;
expect(dao._imapClient).to.equal(dao._imapClient);
expect(dao._smtpClient).to.equal(dao._smtpClient);
expect(dao._account.folders).to.deep.equal(folders);
done();
});
});
});
describe('unlock', function() {
it('should unlock', function(done) {
var importMatcher = sinon.match(function(o) {
@ -362,6 +425,17 @@ define(function(require) {
});
describe('_imapLogin', function() {
it('should fail when disconnected', function(done) {
dao.onDisconnect(null, function(err) {
expect(err).to.not.exist;
dao._imapLogin(function(err) {
expect(err.code).to.equal(42);
done();
});
});
});
it('should work', function(done) {
imapClientStub.login.yields();
@ -382,6 +456,17 @@ define(function(require) {
});
describe('_imapLogout', function() {
it('should fail when disconnected', function(done) {
dao.onDisconnect(null, function(err) {
expect(err).to.not.exist;
dao._imapLogout(function(err) {
expect(err.code).to.equal(42);
done();
});
});
});
it('should work', function(done) {
imapClientStub.logout.yields();
@ -433,6 +518,19 @@ define(function(require) {
});
});
it('should fail when disconnected', function(done) {
devicestorageStub.listItems.yields(null, []);
dao.onDisconnect(null, function(err) {
expect(err).to.not.exist;
dao._imapListFolders(function(err) {
expect(err.code).to.equal(42);
done();
});
});
});
it('should list from imap', function(done) {
devicestorageStub.listItems.yields(null, []);
imapClientStub.listWellKnownFolders.yields(null, {
@ -483,6 +581,17 @@ define(function(require) {
});
describe('_imapListMessages', function() {
it('should fail when disconnected', function(done) {
dao.onDisconnect(null, function(err) {
expect(err).to.not.exist;
dao._imapListMessages({}, function(err) {
expect(err.code).to.equal(42);
done();
});
});
});
it('should work', function(done) {
var path = 'FOLDAAAA';
@ -499,6 +608,17 @@ define(function(require) {
});
describe('_imapDeleteMessage', function() {
it('should fail when disconnected', function(done) {
dao.onDisconnect(null, function(err) {
expect(err).to.not.exist;
dao._imapDeleteMessage({}, function(err) {
expect(err.code).to.equal(42);
done();
});
});
});
it('should work', function(done) {
var path = 'FOLDAAAA',
uid = 1337;
@ -516,6 +636,17 @@ define(function(require) {
});
describe('_imapGetMessage', function() {
it('should fail when disconnected', function(done) {
dao.onDisconnect(null, function(err) {
expect(err).to.not.exist;
dao._imapGetMessage({}, function(err) {
expect(err.code).to.equal(42);
done();
});
});
});
it('should work', function(done) {
var path = 'FOLDAAAA',
uid = 1337;

View File

@ -11,10 +11,9 @@ define(function(require) {
describe('Login Controller unit test', function() {
var scope, location, ctrl, origEmailDao, emailDaoMock,
emailAddress = 'fred@foo.com',
oauthToken = 'foobarfoobar',
startAppStub,
checkForUpdateStub,
fetchOAuthStub,
getEmailAddressStub,
initStub;
describe('initialization', function() {
@ -30,6 +29,11 @@ define(function(require) {
origEmailDao = appController._emailDao;
emailDaoMock = sinon.createStubInstance(EmailDAO);
appController._emailDao = emailDaoMock;
startAppStub = sinon.stub(appController, 'start');
checkForUpdateStub = sinon.stub(appController, 'checkForUpdate');
getEmailAddressStub = sinon.stub(appController, 'getEmailAddress');
initStub = sinon.stub(appController, 'init');
});
afterEach(function() {
@ -49,18 +53,16 @@ define(function(require) {
appController.fetchOAuthToken.restore && appController.fetchOAuthToken.restore();
appController.init.restore && appController.init.restore();
location.path.restore && location.path.restore();
startAppStub.restore();
checkForUpdateStub.restore();
getEmailAddressStub.restore();
initStub.restore();
});
it('should forward to existing user login', function(done) {
startAppStub = sinon.stub(appController, 'start');
startAppStub.yields();
checkForUpdateStub = sinon.stub(appController, 'checkForUpdate');
fetchOAuthStub = sinon.stub(appController, 'fetchOAuthToken');
fetchOAuthStub.yields(null, {
emailAddress: emailAddress,
token: oauthToken
});
initStub = sinon.stub(appController, 'init');
getEmailAddressStub.yields(null, emailAddress);
initStub.yields(null, {
privateKey: 'a',
publicKey: 'b'
@ -74,7 +76,7 @@ define(function(require) {
expect(path).to.equal('/login-existing');
expect(startAppStub.calledOnce).to.be.true;
expect(checkForUpdateStub.calledOnce).to.be.true;
expect(fetchOAuthStub.calledOnce).to.be.true;
expect(getEmailAddressStub.calledOnce).to.be.true;
done();
});
scope = $rootScope.$new();
@ -87,15 +89,8 @@ define(function(require) {
});
it('should forward to new device login', function(done) {
startAppStub = sinon.stub(appController, 'start');
startAppStub.yields();
checkForUpdateStub = sinon.stub(appController, 'checkForUpdate');
fetchOAuthStub = sinon.stub(appController, 'fetchOAuthToken');
fetchOAuthStub.yields(null, {
emailAddress: emailAddress,
token: oauthToken
});
initStub = sinon.stub(appController, 'init');
getEmailAddressStub.yields(null, emailAddress);
initStub.yields(null, {
publicKey: 'b'
});
@ -108,7 +103,7 @@ define(function(require) {
expect(path).to.equal('/login-new-device');
expect(startAppStub.calledOnce).to.be.true;
expect(checkForUpdateStub.calledOnce).to.be.true;
expect(fetchOAuthStub.calledOnce).to.be.true;
expect(getEmailAddressStub.calledOnce).to.be.true;
done();
});
scope = $rootScope.$new();
@ -121,15 +116,8 @@ define(function(require) {
});
it('should forward to initial login', function(done) {
startAppStub = sinon.stub(appController, 'start');
startAppStub.yields();
checkForUpdateStub = sinon.stub(appController, 'checkForUpdate');
fetchOAuthStub = sinon.stub(appController, 'fetchOAuthToken');
fetchOAuthStub.yields(null, {
emailAddress: emailAddress,
token: oauthToken
});
initStub = sinon.stub(appController, 'init');
getEmailAddressStub.yields(null, emailAddress);
initStub.yields();
angular.module('logintest', []);
@ -140,34 +128,7 @@ define(function(require) {
expect(path).to.equal('/login-initial');
expect(startAppStub.calledOnce).to.be.true;
expect(checkForUpdateStub.calledOnce).to.be.true;
expect(fetchOAuthStub.calledOnce).to.be.true;
done();
});
scope = $rootScope.$new();
scope.state = {};
ctrl = $controller(LoginCtrl, {
$location: location,
$scope: scope
});
});
});
it('should fall back to dev mode', function(done) {
var chromeIdentity;
chromeIdentity = window.chrome.identity;
delete window.chrome.identity;
startAppStub = sinon.stub(appController, 'start');
startAppStub.yields();
angular.module('logintest', []);
mocks.module('logintest');
mocks.inject(function($controller, $rootScope, $location) {
location = $location;
sinon.stub(location, 'path', function(path) {
expect(path).to.equal('/desktop');
window.chrome.identity = chromeIdentity;
expect(getEmailAddressStub.calledOnce).to.be.true;
done();
});
scope = $rootScope.$new();

View File

@ -141,6 +141,42 @@ define(function(require) {
});
describe('send to outbox', function() {
it('should work when offline', function(done) {
var verifyToSpy = sinon.spy(scope, 'verifyTo'),
re = {
from: [{
address: 'pity@dafool'
}],
subject: 'Ermahgerd!',
sentDate: new Date(),
body: 'so much body!'
};
scope.state.nav = {
currentFolder: 'currentFolder'
};
scope.emptyOutbox = function() {};
scope.onError = function(err) {
expect(err).to.not.exist;
expect(scope.state.writer.open).to.be.false;
expect(verifyToSpy.calledOnce).to.be.true;
expect(emailDaoMock.store.calledOnce).to.be.true;
expect(emailDaoMock.sync.calledOnce).to.be.true;
scope.verifyTo.restore();
done();
};
emailDaoMock.store.yields();
emailDaoMock.sync.yields({
code: 42
});
scope.state.writer.write(re);
scope.sendToOutbox();
});
it('should work', function(done) {
var verifyToSpy = sinon.spy(scope, 'verifyTo'),
re = {
@ -173,7 +209,40 @@ define(function(require) {
scope.state.writer.write(re);
scope.sendToOutbox();
});
it('should fail', function(done) {
var verifyToSpy = sinon.spy(scope, 'verifyTo'),
re = {
from: [{
address: 'pity@dafool'
}],
subject: 'Ermahgerd!',
sentDate: new Date(),
body: 'so much body!'
};
scope.state.nav = {
currentFolder: 'currentFolder'
};
scope.emptyOutbox = function() {};
scope.onError = function(err) {
expect(err).to.exist;
expect(scope.state.writer.open).to.be.false;
expect(verifyToSpy.calledOnce).to.be.true;
expect(emailDaoMock.store.calledOnce).to.be.true;
expect(emailDaoMock.sync.calledOnce).to.be.true;
scope.verifyTo.restore();
done();
};
emailDaoMock.store.yields();
emailDaoMock.sync.yields({});
scope.state.writer.write(re);
scope.sendToOutbox();
});
it('should not work and not close the write view', function(done) {