From a2f3e86545707f19ab229addef1fc435f44ab530 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 1 Apr 2014 13:16:39 +0200 Subject: [PATCH] [WO-300] Wrap chrome notifications and identity apis in modules --- src/js/app-controller.js | 310 ++++++------------------- src/js/bo/auth.js | 139 +++++++++++ src/js/controller/add-account.js | 2 +- src/js/controller/login-existing.js | 2 +- src/js/controller/login.js | 2 +- src/js/controller/mail-list.js | 13 +- src/js/controller/navigation.js | 9 +- src/js/dao/rest-dao.js | 6 +- src/js/util/notification.js | 26 +++ src/js/util/oauth.js | 55 +++++ test/new-unit/add-account-ctrl-test.js | 15 +- test/new-unit/app-controller-test.js | 180 +++----------- test/new-unit/auth-test.js | 224 ++++++++++++++++++ test/new-unit/login-ctrl-test.js | 26 +-- test/new-unit/main.js | 2 + test/new-unit/oauth-test.js | 92 ++++++++ test/new-unit/rest-dao-test.js | 4 +- 17 files changed, 673 insertions(+), 434 deletions(-) create mode 100644 src/js/bo/auth.js create mode 100644 src/js/util/notification.js create mode 100644 src/js/util/oauth.js create mode 100644 test/new-unit/auth-test.js create mode 100644 test/new-unit/oauth-test.js diff --git a/src/js/app-controller.js b/src/js/app-controller.js index 341888f..6066c61 100644 --- a/src/js/app-controller.js +++ b/src/js/app-controller.js @@ -4,22 +4,24 @@ define(function(require) { 'use strict'; - var ImapClient = require('imap-client'), - mailreader = require('mailreader'), + var Auth = require('js/bo/auth'), + PGP = require('js/crypto/pgp'), PgpMailer = require('pgpmailer'), - EmailDAO = require('js/dao/email-dao'), - EmailSync = require('js/dao/email-sync'), + OAuth = require('js/util/oauth'), + PgpBuilder = require('pgpbuilder'), + OutboxBO = require('js/bo/outbox'), + mailreader = require('mailreader'), + ImapClient = require('imap-client'), RestDAO = require('js/dao/rest-dao'), + EmailDAO = require('js/dao/email-dao'), + config = require('js/app-config').config, + EmailSync = require('js/dao/email-sync'), + KeychainDAO = require('js/dao/keychain-dao'), PublicKeyDAO = require('js/dao/publickey-dao'), LawnchairDAO = require('js/dao/lawnchair-dao'), - KeychainDAO = require('js/dao/keychain-dao'), - DeviceStorageDAO = require('js/dao/devicestorage-dao'), InvitationDAO = require('js/dao/invitation-dao'), - OutboxBO = require('js/bo/outbox'), - PGP = require('js/crypto/pgp'), - PgpBuilder = require('pgpbuilder'), - UpdateHandler = require('js/util/update/update-handler'), - config = require('js/app-config').config; + DeviceStorageDAO = require('js/dao/devicestorage-dao'), + UpdateHandler = require('js/util/update/update-handler'); var self = {}; @@ -41,64 +43,74 @@ define(function(require) { function onDeviceReady() { console.log('Starting app.'); + self.buildModules(); + // 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.buildModules = function() { + var lawnchairDao, restDao, pubkeyDao, emailDao, emailSync, keychain, pgp, userStorage, pgpbuilder, oauth, appConfigStore; + // start the mailreader's worker thread + mailreader.startWorker(config.workerPath + '/../lib/mailreader-parser-worker.js'); + + // init objects and inject dependencies + restDao = new RestDAO(); + lawnchairDao = new LawnchairDAO(); + pubkeyDao = new PublicKeyDAO(restDao); + oauth = new OAuth(new RestDAO('https://www.googleapis.com')); + + self._appConfigStore = appConfigStore = new DeviceStorageDAO(new LawnchairDAO()); + self._auth = new Auth(appConfigStore, oauth, new RestDAO('/ca')); + self._userStorage = userStorage = new DeviceStorageDAO(lawnchairDao); + self._invitationDao = new InvitationDAO(restDao); + self._keychain = keychain = new KeychainDAO(lawnchairDao, pubkeyDao); + self._crypto = pgp = new PGP(); + self._pgpbuilder = pgpbuilder = new PgpBuilder(); + emailSync = new EmailSync(keychain, userStorage); + self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader, emailSync); + self._outboxBo = new OutboxBO(emailDao, keychain, userStorage); + self._updateHandler = new UpdateHandler(appConfigStore, userStorage); + }; + + self.isOnline = function() { + return navigator.onLine; + }; + + self.onDisconnect = function(callback) { 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()) { + if (!self.isOnline() || !self._emailDao._account) { // 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) { + self._auth.getCredentials({}, function(err, credentials) { if (err) { callback(err); return; } - // get a fresh oauth token - self.fetchOAuthToken(function(err, oauth) { - if (err) { - callback(err); - return; - } - - initClients(oauth, certificate); - }); + initClients(credentials); }); - function initClients(oauth, certificate) { + function initClients(credentials) { var auth, imapOptions, imapClient, smtpOptions, pgpMailer; auth = { XOAuth2: { - user: oauth.emailAddress, + user: credentials.emailAddress, clientId: config.gmail.clientId, - accessToken: oauth.token + accessToken: credentials.oauthToken } }; imapOptions = { @@ -106,7 +118,7 @@ define(function(require) { port: config.gmail.imap.port, host: config.gmail.imap.host, auth: auth, - ca: [certificate] + ca: [credentials.sslCert] }; smtpOptions = { secureConnection: config.gmail.smtp.secure, @@ -114,27 +126,14 @@ define(function(require) { host: config.gmail.smtp.host, auth: auth, tls: { - ca: [certificate] + ca: [credentials.sslCert] }, onError: console.error }; - imapClient = new ImapClient(imapOptions, mailreader); pgpMailer = new PgpMailer(smtpOptions, self._pgpbuilder); - - imapClient.onError = function(err) { - console.log('IMAP error.', err); - console.log('IMAP reconnecting...'); - // re-init client modules on error - self.onConnect(function(err) { - if (err) { - console.error('IMAP reconnect failed!', err); - return; - } - - console.log('IMAP reconnect attempt complete.'); - }); - }; + imapClient = new ImapClient(imapOptions, mailreader); + imapClient.onError = onImapError; // connect to clients self._emailDao.onConnect({ @@ -142,42 +141,24 @@ define(function(require) { pgpMailer: pgpMailer }, callback); } - }; - self.getCertficate = function(localCallback) { - if (self.certificate) { - localCallback(null, self.certificate); - return; + function onImapError(err) { + console.log('IMAP error.', err); + console.log('IMAP reconnecting...'); + // re-init client modules on error + self.onConnect(function(err) { + if (err) { + console.error('IMAP reconnect failed!', err); + return; + } + + console.log('IMAP reconnect attempt complete.'); + }); } - - // fetch pinned local ssl certificate - var ca = new RestDAO({ - baseUri: '/ca' - }); - - ca.get({ - uri: '/Google_Internet_Authority_G2.pem', - type: 'text' - }, function(err, cert) { - if (err || !cert) { - localCallback({ - errMsg: 'Could not fetch pinned certificate!' - }); - return; - } - - self.certificate = cert; - localCallback(null, self.certificate); - return; - }); - }; - - self.isOnline = function() { - return navigator.onLine; }; self.checkForUpdate = function() { - if (!chrome || !chrome.runtime || !chrome.runtime.onUpdateAvailable) { + if (!window.chrome || !chrome.runtime || !chrome.runtime.onUpdateAvailable) { return; } @@ -197,167 +178,10 @@ 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 && !self.isOnline()) { - // first time login... must be online - callback({ - errMsg: 'The app must be online on first use!' - }); - return; - } - - callback(null, cachedEmailAddress); - }); - }; - - /** - * 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 - var googleEndpoint = new RestDAO({ - baseUri: 'https://www.googleapis.com' - }); - - googleEndpoint.get({ - uri: '/oauth2/v1/tokeninfo?access_token=' + token - }, function(err, info) { - if (err || !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); - }); - }); - } - }; - - /** - * Request an OAuth token from chrome for gmail users - */ - self.fetchOAuthToken = function(callback) { - // get OAuth Token from chrome - chrome.identity.getAuthToken({ - 'interactive': true - }, onToken); - - function onToken(token) { - if ((chrome && chrome.runtime && chrome.runtime.lastError) || !token) { - callback({ - errMsg: 'Error fetching an OAuth token for the user!' - }); - return; - } - - // get email address for the token - self.queryEmailAddress(token, function(err, emailAddress) { - if (err || !emailAddress) { - callback({ - errMsg: 'Error looking up email address on login!', - err: err - }); - return; - } - - // init the email dao - callback(null, { - emailAddress: emailAddress, - token: token - }); - }); - } - }; - - self.buildModules = function() { - var lawnchairDao, restDao, pubkeyDao, emailDao, emailSync, keychain, pgp, userStorage, pgpbuilder; - - // start the mailreader's worker thread - mailreader.startWorker(config.workerPath + '/../lib/mailreader-parser-worker.js'); - - // init objects and inject dependencies - restDao = new RestDAO(); - pubkeyDao = new PublicKeyDAO(restDao); - lawnchairDao = new LawnchairDAO(); - - self._userStorage = userStorage = new DeviceStorageDAO(lawnchairDao); - self._invitationDao = new InvitationDAO(restDao); - self._keychain = keychain = new KeychainDAO(lawnchairDao, pubkeyDao); - self._crypto = pgp = new PGP(); - self._pgpbuilder = pgpbuilder = new PgpBuilder(); - emailSync = new EmailSync(keychain, userStorage); - self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader, emailSync); - self._outboxBo = new OutboxBO(emailDao, keychain, userStorage); - self._updateHandler = new UpdateHandler(self._appConfigStore, userStorage); - }; - /** * Instanciate the mail email data access object and its dependencies. Login to imap on init. */ self.init = function(options, callback) { - self.buildModules(); - // init user's local database self._userStorage.init(options.emailAddress, function(err) { if (err) { diff --git a/src/js/bo/auth.js b/src/js/bo/auth.js new file mode 100644 index 0000000..5fe7f79 --- /dev/null +++ b/src/js/bo/auth.js @@ -0,0 +1,139 @@ +define(function() { + 'use strict'; + + var emailItemKey = 'emailaddress'; + + var Auth = function(appConfigStore, oauth, ca) { + this._appConfigStore = appConfigStore; + this._oauth = oauth; + this._ca = ca; + }; + + Auth.prototype.getCredentials = function(options, callback) { + var self = this; + + // fetch pinned local ssl certificate + self.getCertificate(function(err, certificate) { + if (err) { + callback(err); + return; + } + + // get a fresh oauth token + self._oauth.getOAuthToken(function(err, token) { + if (err) { + callback(err); + return; + } + + // get email address for the token + self.queryEmailAddress(token, function(err, emailAddress) { + if (err) { + callback(err); + return; + } + + callback(null, { + emailAddress: emailAddress, + oauthToken: token, + sslCert: certificate + }); + }); + }); + }); + }; + + /** + * Get the pinned ssl certificate for the corresponding mail server. + */ + Auth.prototype.getCertificate = function(callback) { + this._ca.get({ + uri: '/Google_Internet_Authority_G2.pem', + type: 'text' + }, function(err, cert) { + if (err || !cert) { + callback({ + errMsg: 'Could not fetch pinned certificate!' + }); + return; + } + + callback(null, cert); + }); + }; + + /** + * Gracefully try to fetch the user's email address from local storage. + * If not yet stored, handle online/offline cases on first use. + */ + Auth.prototype.getEmailAddress = function(callback) { + // try to fetch email address from local storage + this.getEmailAddressFromConfig(function(err, cachedEmailAddress) { + if (err) { + callback(err); + return; + } + + callback(null, cachedEmailAddress); + }); + }; + + /** + * Get the user's email address from local storage + */ + Auth.prototype.getEmailAddressFromConfig = function(callback) { + this._appConfigStore.listItems(emailItemKey, 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 + */ + Auth.prototype.queryEmailAddress = function(token, callback) { + var self = this; + + self.getEmailAddressFromConfig(function(err, cachedEmailAddress) { + if (err) { + callback(err); + return; + } + + // do roundtrip to google api if no email address is cached yet + if (!cachedEmailAddress) { + queryOAuthApi(); + return; + } + + callback(null, cachedEmailAddress); + }); + + function queryOAuthApi() { + self._oauth.queryEmailAddress(token, function(err, emailAddress) { + if (err) { + callback(err); + return; + } + + // cache the email address on the device + self._appConfigStore.storeList([emailAddress], emailItemKey, function(err) { + callback(err, emailAddress); + }); + }); + } + }; + + return Auth; +}); \ No newline at end of file diff --git a/src/js/controller/add-account.js b/src/js/controller/add-account.js index 12b21ab..cbcf2c0 100644 --- a/src/js/controller/add-account.js +++ b/src/js/controller/add-account.js @@ -11,7 +11,7 @@ define(function(require) { errorUtil.attachHandler($scope); $scope.connectToGoogle = function() { - appController.fetchOAuthToken(function(err) { + appController._auth.getCredentials({}, function(err) { if (err) { $scope.onError(err); return; diff --git a/src/js/controller/login-existing.js b/src/js/controller/login-existing.js index f5cdbf2..9fd14b3 100644 --- a/src/js/controller/login-existing.js +++ b/src/js/controller/login-existing.js @@ -32,7 +32,7 @@ define(function(require) { function unlockCrypto() { var userId = emailDao._account.emailAddress; - appController._emailDao._keychain.getUserKeyPair(userId, function(err, keypair) { + emailDao._keychain.getUserKeyPair(userId, function(err, keypair) { if (err) { handleError(err); return; diff --git a/src/js/controller/login.js b/src/js/controller/login.js index ba22ba1..39fd912 100644 --- a/src/js/controller/login.js +++ b/src/js/controller/login.js @@ -27,7 +27,7 @@ define(function(require) { function initializeUser() { // get OAuth token from chrome - appController.getEmailAddress(function(err, emailAddress) { + appController._auth.getEmailAddress(function(err, emailAddress) { if (err) { $scope.onError(err); return; diff --git a/src/js/controller/mail-list.js b/src/js/controller/mail-list.js index 0a96826..a8f1c18 100644 --- a/src/js/controller/mail-list.js +++ b/src/js/controller/mail-list.js @@ -6,7 +6,7 @@ define(function(require) { appController = require('js/app-controller'), IScroll = require('iscroll'), str = require('js/app-config').string, - cfg = require('js/app-config').config, + notification = require('js/util/notification'), emailDao, outboxBo; var MailListCtrl = function($scope) { @@ -26,7 +26,7 @@ define(function(require) { notificationForEmail(email); }); }; - chrome.notifications.onClicked.addListener(notificationClicked); + notification.setOnClickedListener(notificationClicked); } // @@ -38,7 +38,7 @@ define(function(require) { folder: getFolder().path, message: email }, function(err) { - if (err) { + if (err && err.code !== 42) { $scope.onError(err); return; } @@ -244,11 +244,10 @@ define(function(require) { } function notificationForEmail(email) { - chrome.notifications.create('' + email.uid, { - type: 'basic', + notification.create({ + id: '' + email.uid, title: email.from[0].name || email.from[0].address, - message: email.subject.replace(str.subjectPrefix, ''), - iconUrl: chrome.runtime.getURL(cfg.iconPath) + message: email.subject.replace(str.subjectPrefix, '') }, function() {}); } diff --git a/src/js/controller/navigation.js b/src/js/controller/navigation.js index 1deb6fb..1c918ee 100644 --- a/src/js/controller/navigation.js +++ b/src/js/controller/navigation.js @@ -3,9 +3,9 @@ define(function(require) { var angular = require('angular'), str = require('js/app-config').string, - cfg = require('js/app-config').config, appController = require('js/app-controller'), errorUtil = require('js/util/error'), + notification = require('js/util/notification'), _ = require('underscore'), emailDao, outboxBo; @@ -126,11 +126,10 @@ define(function(require) { } function sentNotification(email) { - chrome.notifications.create('o' + email.id, { - type: 'basic', + notification.create({ + id: 'o' + email.id, title: 'Message sent', - message: email.subject.replace(str.subjectPrefix, ''), - iconUrl: chrome.runtime.getURL(cfg.iconPath) + message: email.subject.replace(str.subjectPrefix, '') }, function() {}); } }; diff --git a/src/js/dao/rest-dao.js b/src/js/dao/rest-dao.js index d70fa60..581747d 100644 --- a/src/js/dao/rest-dao.js +++ b/src/js/dao/rest-dao.js @@ -3,9 +3,9 @@ define(function(require) { var config = require('js/app-config').config; - var RestDAO = function(options) { - if (options && options.baseUri) { - this._baseUri = options.baseUri; + var RestDAO = function(baseUri) { + if (baseUri) { + this._baseUri = baseUri; } else { this._baseUri = config.cloudUrl; } diff --git a/src/js/util/notification.js b/src/js/util/notification.js new file mode 100644 index 0000000..a5715f4 --- /dev/null +++ b/src/js/util/notification.js @@ -0,0 +1,26 @@ +define(function(require) { + 'use strict'; + + var cfg = require('js/app-config').config; + + var self = {}; + + self.create = function(options, callback) { + if (window.chrome && chrome.notifications) { + chrome.notifications.create(options.id, { + type: 'basic', + title: options.title, + message: options.message, + iconUrl: chrome.runtime.getURL(cfg.iconPath) + }, callback); + } + }; + + self.setOnClickedListener = function(listener) { + if (window.chrome && chrome.notifications) { + chrome.notifications.onClicked.addListener(listener); + } + }; + + return self; +}); \ No newline at end of file diff --git a/src/js/util/oauth.js b/src/js/util/oauth.js new file mode 100644 index 0000000..f37af8c --- /dev/null +++ b/src/js/util/oauth.js @@ -0,0 +1,55 @@ +define(function() { + 'use strict'; + + var OAuth = function(googleApi) { + this._googleApi = googleApi; + }; + + OAuth.prototype.isSupported = function() { + return !!(window.chrome && chrome.identity); + }; + + /** + * Request an OAuth token from chrome for gmail users + */ + OAuth.prototype.getOAuthToken = function(callback) { + // get OAuth Token from chrome + chrome.identity.getAuthToken({ + 'interactive': true + }, function(token) { + if ((chrome && chrome.runtime && chrome.runtime.lastError) || !token) { + callback({ + errMsg: 'Error fetching an OAuth token for the user!' + }); + return; + } + + callback(null, token); + }); + }; + + OAuth.prototype.queryEmailAddress = function(token, callback) { + if (!token) { + callback({ + errMsg: 'Invalid OAuth token!' + }); + return; + } + + // fetch gmail user's email address from the Google Authorization Server + this._googleApi.get({ + uri: '/oauth2/v1/tokeninfo?access_token=' + token + }, function(err, info) { + if (err || !info || !info.email) { + callback({ + errMsg: 'Error looking up email address on google api!' + }); + return; + } + + callback(null, info.email); + }); + }; + + return OAuth; +}); \ No newline at end of file diff --git a/test/new-unit/add-account-ctrl-test.js b/test/new-unit/add-account-ctrl-test.js index 3f2ce4f..803bc35 100644 --- a/test/new-unit/add-account-ctrl-test.js +++ b/test/new-unit/add-account-ctrl-test.js @@ -5,22 +5,21 @@ define(function(require) { angular = require('angular'), mocks = require('angularMocks'), AddAccountCtrl = require('js/controller/add-account'), + Auth = require('js/bo/auth'), appController = require('js/app-controller'); describe('Add Account Controller unit test', function() { - var scope, location, ctrl, - fetchOAuthTokenStub; + var scope, location, ctrl, authStub; describe('connectToGoogle', function() { beforeEach(function() { // remember original module to restore later, then replace it - fetchOAuthTokenStub = sinon.stub(appController, 'fetchOAuthToken'); + appController._auth = authStub = sinon.createStubInstance(Auth); }); afterEach(function() { // restore the app controller module location && location.path && location.path.restore && location.path.restore(); - fetchOAuthTokenStub.restore(); }); it('should fail on fetchOAuthToken error', function(done) { @@ -37,10 +36,10 @@ define(function(require) { scope.onError = function(err) { expect(err).to.equal(42); - expect(fetchOAuthTokenStub.calledOnce).to.be.true; + expect(authStub.getCredentials.calledOnce).to.be.true; done(); }; - fetchOAuthTokenStub.yields(42); + authStub.getCredentials.yields(42); scope.connectToGoogle(); }); @@ -55,7 +54,7 @@ define(function(require) { sinon.stub(location, 'path', function(path) { expect(path).to.equal('/login'); - expect(fetchOAuthTokenStub.calledOnce).to.be.true; + expect(authStub.getCredentials.calledOnce).to.be.true; location.path.restore(); scope.$apply.restore(); @@ -70,7 +69,7 @@ define(function(require) { }); }); - fetchOAuthTokenStub.yields(); + authStub.getCredentials.yields(); scope.connectToGoogle(); }); diff --git a/test/new-unit/app-controller-test.js b/test/new-unit/app-controller-test.js index b3944cb..c89c1cd 100644 --- a/test/new-unit/app-controller-test.js +++ b/test/new-unit/app-controller-test.js @@ -6,11 +6,11 @@ define(function(require) { OutboxBO = require('js/bo/outbox'), DeviceStorageDAO = require('js/dao/devicestorage-dao'), UpdateHandler = require('js/util/update/update-handler'), + Auth = require('js/bo/auth'), expect = chai.expect; describe('App Controller unit tests', function() { - var emailDaoStub, outboxStub, updateHandlerStub, appConfigStoreStub, devicestorageStub, isOnlineStub, - identityStub; + var emailDaoStub, outboxStub, updateHandlerStub, appConfigStoreStub, devicestorageStub, isOnlineStub, authStub; beforeEach(function() { controller._emailDao = emailDaoStub = sinon.createStubInstance(EmailDAO); @@ -18,22 +18,31 @@ define(function(require) { controller._appConfigStore = appConfigStoreStub = sinon.createStubInstance(DeviceStorageDAO); controller._userStorage = devicestorageStub = sinon.createStubInstance(DeviceStorageDAO); controller._updateHandler = updateHandlerStub = sinon.createStubInstance(UpdateHandler); + controller._auth = authStub = sinon.createStubInstance(Auth); isOnlineStub = sinon.stub(controller, 'isOnline'); - - window.chrome = window.chrome || {}; - window.chrome.identity = window.chrome.identity || {}; - if (typeof window.chrome.identity.getAuthToken !== 'function') { - window.chrome.identity.getAuthToken = function() {}; - } - identityStub = sinon.stub(window.chrome.identity, 'getAuthToken'); }); afterEach(function() { - identityStub.restore(); isOnlineStub.restore(); }); + describe('buildModules', function() { + it('should work', function() { + controller.buildModules(); + expect(controller._appConfigStore).to.exist; + expect(controller._auth).to.exist; + expect(controller._userStorage).to.exist; + expect(controller._invitationDao).to.exist; + expect(controller._keychain).to.exist; + expect(controller._crypto).to.exist; + expect(controller._pgpbuilder).to.exist; + expect(controller._emailDao).to.exist; + expect(controller._outboxBo).to.exist; + expect(controller._updateHandler).to.exist; + }); + }); + describe('start', function() { it('should not explode', function(done) { controller.start({ @@ -57,17 +66,8 @@ define(function(require) { }); 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(); + controller._emailDao._account = {}; }); it('should not connect if offline', function(done) { @@ -79,171 +79,55 @@ define(function(require) { }); }); - it('should fail due to error in certificate', function(done) { - isOnlineStub.returns(true); - getCertficateStub.yields({}); + it('should not connect if account is not initialized', function(done) { + controller._emailDao._account = null; controller.onConnect(function(err) { - expect(err).to.exist; - expect(getCertficateStub.calledOnce).to.be.true; + expect(err).to.not.exist; done(); }); }); - it('should fail due to error in fetch oauth', function(done) { + it('should fail due to error in auth.getCredentials', function(done) { isOnlineStub.returns(true); - getCertficateStub.yields(null, 'PEM'); - fetchOAuthTokenStub.yields({}); + authStub.getCredentials.withArgs({}).yields(new Error()); controller.onConnect(function(err) { expect(err).to.exist; - expect(fetchOAuthTokenStub.calledOnce).to.be.true; - expect(getCertficateStub.calledOnce).to.be.true; + expect(authStub.getCredentials.calledOnce).to.be.true; done(); }); }); it('should work', function(done) { isOnlineStub.returns(true); - fetchOAuthTokenStub.yields(null, { - emailAddress: 'asfd@example.com' + authStub.getCredentials.withArgs({}).yields(null, { + emailAddress: 'asdf@example.com', + oauthToken: 'token', + sslCert: 'cert' }); - 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(authStub.getCredentials.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(); - }); - }); - }); - - describe('fetchOAuthToken', function() { - var queryEmailAddressStub; - - beforeEach(function() { - // buildModules - queryEmailAddressStub = sinon.stub(controller, 'queryEmailAddress'); - }); - - afterEach(function() { - queryEmailAddressStub.restore(); - }); - - it('should work', function(done) { - identityStub.yields('token42'); - queryEmailAddressStub.yields(null, 'bob@asdf.com'); - - controller.fetchOAuthToken(function(err, res) { - expect(err).to.not.exist; - expect(res.emailAddress).to.equal('bob@asdf.com'); - expect(res.token).to.equal('token42'); - expect(queryEmailAddressStub.calledOnce).to.be.true; - expect(identityStub.calledOnce).to.be.true; - done(); - }); - }); - - it('should fail due to chrome api error', function(done) { - identityStub.yields(); - - controller.fetchOAuthToken(function(err) { - expect(err).to.exist; - expect(identityStub.calledOnce).to.be.true; - done(); - }); - }); - - it('should fail due error querying email address', function(done) { - identityStub.yields('token42'); - queryEmailAddressStub.yields(); - - controller.fetchOAuthToken(function(err) { - expect(err).to.exist; - expect(queryEmailAddressStub.calledOnce).to.be.true; - expect(identityStub.calledOnce).to.be.true; - done(); - }); - }); - }); - - describe('buildModules', function() { - it('should work', function() { - controller.buildModules(); - expect(controller._userStorage).to.exist; - expect(controller._invitationDao).to.exist; - expect(controller._keychain).to.exist; - expect(controller._crypto).to.exist; - expect(controller._pgpbuilder).to.exist; - expect(controller._emailDao).to.exist; - expect(controller._outboxBo).to.exist; - expect(controller._updateHandler).to.exist; - }); - }); - describe('init', function() { - var buildModulesStub, onConnectStub, emailAddress; + var onConnectStub, emailAddress; beforeEach(function() { emailAddress = 'alice@bob.com'; - // buildModules - buildModulesStub = sinon.stub(controller, 'buildModules'); - buildModulesStub.returns(); // onConnect onConnectStub = sinon.stub(controller, 'onConnect'); }); afterEach(function() { - buildModulesStub.restore(); onConnectStub.restore(); }); diff --git a/test/new-unit/auth-test.js b/test/new-unit/auth-test.js new file mode 100644 index 0000000..9d39b51 --- /dev/null +++ b/test/new-unit/auth-test.js @@ -0,0 +1,224 @@ +define(function(require) { + 'use strict'; + + var Auth = require('js/bo/auth'), + OAuth = require('js/util/oauth'), + RestDAO = require('js/dao/rest-dao'), + DeviceStorageDAO = require('js/dao/devicestorage-dao'), + expect = chai.expect; + + describe('Auth unit tests', function() { + var auth, appConfigStoreStub, oauthStub, caStub; + + beforeEach(function() { + appConfigStoreStub = sinon.createStubInstance(DeviceStorageDAO); + oauthStub = sinon.createStubInstance(OAuth); + caStub = sinon.createStubInstance(RestDAO); + auth = new Auth(appConfigStoreStub, oauthStub, caStub); + }); + + afterEach(function() {}); + + describe('getCredentials', function() { + var getCertificateStub, queryEmailAddressStub; + + beforeEach(function() { + getCertificateStub = sinon.stub(auth, 'getCertificate'); + queryEmailAddressStub = sinon.stub(auth, 'queryEmailAddress'); + }); + + it('should work', function(done) { + getCertificateStub.yields(null, 'cert'); + queryEmailAddressStub.withArgs('token').yields(null, 'asdf@example.com'); + oauthStub.getOAuthToken.yields(null, 'token'); + + auth.getCredentials({}, function(err, credentials) { + expect(err).to.not.exist; + expect(credentials.emailAddress).to.equal('asdf@example.com'); + expect(credentials.oauthToken).to.equal('token'); + expect(credentials.sslCert).to.equal('cert'); + done(); + }); + }); + + it('should fail due to error in getCertificate', function(done) { + getCertificateStub.yields(new Error()); + + auth.getCredentials({}, function(err, credentials) { + expect(err).to.exist; + expect(credentials).to.not.exist; + done(); + }); + }); + + it('should fail due to error in getOAuthToken', function(done) { + getCertificateStub.yields(null, 'cert'); + oauthStub.getOAuthToken.yields(new Error()); + + auth.getCredentials({}, function(err, credentials) { + expect(err).to.exist; + expect(credentials).to.not.exist; + done(); + }); + }); + + it('should fail due to error in queryEmailAddress', function(done) { + getCertificateStub.yields(null, 'cert'); + queryEmailAddressStub.withArgs('token').yields(new Error()); + oauthStub.getOAuthToken.yields(null, 'token'); + + auth.getCredentials({}, function(err, credentials) { + expect(err).to.exist; + expect(credentials).to.not.exist; + done(); + }); + }); + }); + + describe('getCertificate', function() { + it('should work', function(done) { + caStub.get.yields(null, 'cert'); + + auth.getCertificate(function(err, cert) { + expect(err).to.not.exist; + expect(cert).to.equal('cert'); + done(); + }); + }); + + it('should fail', function(done) { + caStub.get.yields(null, ''); + + auth.getCertificate(function(err, cert) { + expect(err).to.exist; + expect(cert).to.not.exist; + done(); + }); + }); + }); + + describe('getEmailAddress', function() { + var getEmailAddressFromConfigStub; + + beforeEach(function() { + getEmailAddressFromConfigStub = sinon.stub(auth, 'getEmailAddressFromConfig'); + }); + + it('should work', function(done) { + getEmailAddressFromConfigStub.yields(null, 'asdf@example.com'); + + auth.getEmailAddress(function(err, emailAddress) { + expect(err).to.not.exist; + expect(emailAddress).to.equal('asdf@example.com'); + done(); + }); + }); + + it('should fail', function(done) { + getEmailAddressFromConfigStub.yields(new Error()); + + auth.getEmailAddress(function(err, emailAddress) { + expect(err).to.exist; + expect(emailAddress).to.not.exist; + done(); + }); + }); + }); + + describe('getEmailAddressFromConfig', function() { + it('should work', function(done) { + appConfigStoreStub.listItems.withArgs('emailaddress', 0, null).yields(null, ['asdf@example.com']); + + auth.getEmailAddressFromConfig(function(err, emailAddress) { + expect(err).to.not.exist; + expect(emailAddress).to.equal('asdf@example.com'); + done(); + }); + }); + + it('should return empty result', function(done) { + appConfigStoreStub.listItems.withArgs('emailaddress', 0, null).yields(null, []); + + auth.getEmailAddressFromConfig(function(err, emailAddress) { + expect(err).to.not.exist; + expect(emailAddress).to.not.exist; + done(); + }); + }); + + it('should fail', function(done) { + appConfigStoreStub.listItems.withArgs('emailaddress', 0, null).yields(new Error()); + + auth.getEmailAddressFromConfig(function(err, emailAddress) { + expect(err).to.exist; + expect(emailAddress).to.not.exist; + done(); + }); + }); + }); + + describe('queryEmailAddress', function() { + var getEmailAddressFromConfigStub; + + beforeEach(function() { + getEmailAddressFromConfigStub = sinon.stub(auth, 'getEmailAddressFromConfig'); + }); + + it('should if already cached', function(done) { + getEmailAddressFromConfigStub.yields(null, 'asdf@example.com'); + + auth.queryEmailAddress('token', function(err, emailAddress) { + expect(err).to.not.exist; + expect(emailAddress).to.equal('asdf@example.com'); + done(); + }); + }); + + it('should when querying oauth api', function(done) { + getEmailAddressFromConfigStub.yields(); + oauthStub.queryEmailAddress.withArgs('token').yields(null, 'asdf@example.com'); + appConfigStoreStub.storeList.withArgs(['asdf@example.com'], 'emailaddress').yields(); + + auth.queryEmailAddress('token', function(err, emailAddress) { + expect(err).to.not.exist; + expect(emailAddress).to.equal('asdf@example.com'); + done(); + }); + }); + + it('should fail due to error in cache lookup', function(done) { + getEmailAddressFromConfigStub.yields(new Error()); + + auth.queryEmailAddress('token', function(err, emailAddress) { + expect(err).to.exist; + expect(emailAddress).to.not.exist; + done(); + }); + }); + + it('should fail due to error in oauth api', function(done) { + getEmailAddressFromConfigStub.yields(); + oauthStub.queryEmailAddress.withArgs('token').yields(new Error()); + + auth.queryEmailAddress('token', function(err, emailAddress) { + expect(err).to.exist; + expect(emailAddress).to.not.exist; + done(); + }); + }); + + it('should fail due to error in oauth api', function(done) { + getEmailAddressFromConfigStub.yields(); + oauthStub.queryEmailAddress.withArgs('token').yields(null, 'asdf@example.com'); + appConfigStoreStub.storeList.withArgs(['asdf@example.com'], 'emailaddress').yields(new Error()); + + auth.queryEmailAddress('token', function(err, emailAddress) { + expect(err).to.exist; + expect(emailAddress).to.exist; + done(); + }); + }); + }); + + }); +}); \ No newline at end of file diff --git a/test/new-unit/login-ctrl-test.js b/test/new-unit/login-ctrl-test.js index 7c858f7..9ca206b 100644 --- a/test/new-unit/login-ctrl-test.js +++ b/test/new-unit/login-ctrl-test.js @@ -6,6 +6,7 @@ define(function(require) { mocks = require('angularMocks'), LoginCtrl = require('js/controller/login'), EmailDAO = require('js/dao/email-dao'), + Auth = require('js/bo/auth'), appController = require('js/app-controller'); describe('Login Controller unit test', function() { @@ -13,7 +14,7 @@ define(function(require) { emailAddress = 'fred@foo.com', startAppStub, checkForUpdateStub, - getEmailAddressStub, + authStub, initStub; describe('initialization', function() { @@ -27,12 +28,11 @@ define(function(require) { // remember original module to restore later, then replace it origEmailDao = appController._emailDao; - emailDaoMock = sinon.createStubInstance(EmailDAO); - appController._emailDao = emailDaoMock; + appController._emailDao = emailDaoMock = sinon.createStubInstance(EmailDAO); + appController._auth = authStub = sinon.createStubInstance(Auth); startAppStub = sinon.stub(appController, 'start'); checkForUpdateStub = sinon.stub(appController, 'checkForUpdate'); - getEmailAddressStub = sinon.stub(appController, 'getEmailAddress'); initStub = sinon.stub(appController, 'init'); }); @@ -50,13 +50,11 @@ define(function(require) { appController._emailDao = origEmailDao; appController.start.restore && appController.start.restore(); appController.checkForUpdate.restore && appController.checkForUpdate.restore(); - 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(); }); @@ -67,7 +65,7 @@ define(function(require) { }; startAppStub.yields(); - getEmailAddressStub.yields(null, emailAddress); + authStub.getEmailAddress.yields(null, emailAddress); initStub.yields(null, testKeys); emailDaoMock.unlock.withArgs({ @@ -83,7 +81,7 @@ define(function(require) { expect(path).to.equal('/desktop'); expect(startAppStub.calledOnce).to.be.true; expect(checkForUpdateStub.calledOnce).to.be.true; - expect(getEmailAddressStub.calledOnce).to.be.true; + expect(authStub.getEmailAddress.calledOnce).to.be.true; done(); }); scope = $rootScope.$new(); @@ -102,7 +100,7 @@ define(function(require) { }; startAppStub.yields(); - getEmailAddressStub.yields(null, emailAddress); + authStub.getEmailAddress.yields(null, emailAddress); initStub.yields(null, testKeys); emailDaoMock.unlock.withArgs({ @@ -118,7 +116,7 @@ define(function(require) { expect(path).to.equal('/login-existing'); expect(startAppStub.calledOnce).to.be.true; expect(checkForUpdateStub.calledOnce).to.be.true; - expect(getEmailAddressStub.calledOnce).to.be.true; + expect(authStub.getEmailAddress.calledOnce).to.be.true; done(); }); scope = $rootScope.$new(); @@ -132,7 +130,7 @@ define(function(require) { it('should forward to new device login', function(done) { startAppStub.yields(); - getEmailAddressStub.yields(null, emailAddress); + authStub.getEmailAddress.yields(null, emailAddress); initStub.yields(null, { publicKey: 'b' }); @@ -145,7 +143,7 @@ define(function(require) { expect(path).to.equal('/login-new-device'); expect(startAppStub.calledOnce).to.be.true; expect(checkForUpdateStub.calledOnce).to.be.true; - expect(getEmailAddressStub.calledOnce).to.be.true; + expect(authStub.getEmailAddress.calledOnce).to.be.true; done(); }); scope = $rootScope.$new(); @@ -159,7 +157,7 @@ define(function(require) { it('should forward to initial login', function(done) { startAppStub.yields(); - getEmailAddressStub.yields(null, emailAddress); + authStub.getEmailAddress.yields(null, emailAddress); initStub.yields(); angular.module('logintest', []); @@ -170,7 +168,7 @@ define(function(require) { expect(path).to.equal('/login-initial'); expect(startAppStub.calledOnce).to.be.true; expect(checkForUpdateStub.calledOnce).to.be.true; - expect(getEmailAddressStub.calledOnce).to.be.true; + expect(authStub.getEmailAddress.calledOnce).to.be.true; done(); }); scope = $rootScope.$new(); diff --git a/test/new-unit/main.js b/test/new-unit/main.js index 7cb53c6..c8d4cf9 100644 --- a/test/new-unit/main.js +++ b/test/new-unit/main.js @@ -28,6 +28,8 @@ function startTests() { require( [ + 'test/new-unit/oauth-test', + 'test/new-unit/auth-test', 'test/new-unit/email-dao-test', 'test/new-unit/email-sync-test', 'test/new-unit/app-controller-test', diff --git a/test/new-unit/oauth-test.js b/test/new-unit/oauth-test.js new file mode 100644 index 0000000..daea932 --- /dev/null +++ b/test/new-unit/oauth-test.js @@ -0,0 +1,92 @@ +define(function(require) { + 'use strict'; + + var OAuth = require('js/util/oauth'), + RestDAO = require('js/dao/rest-dao'), + expect = chai.expect; + + describe('OAuth unit tests', function() { + var oauth, googleApiStub, identityStub; + + beforeEach(function() { + googleApiStub = sinon.createStubInstance(RestDAO); + oauth = new OAuth(googleApiStub); + + window.chrome = window.chrome || {}; + window.chrome.identity = window.chrome.identity || {}; + if (typeof window.chrome.identity.getAuthToken !== 'function') { + window.chrome.identity.getAuthToken = function() {}; + } + identityStub = sinon.stub(window.chrome.identity, 'getAuthToken'); + }); + + afterEach(function() { + identityStub.restore(); + }); + + describe('isSupported', function() { + it('should work', function() { + expect(oauth.isSupported()).to.be.true; + }); + }); + + describe('getOAuthToken', function() { + it('should work', function(done) { + identityStub.yields('token'); + + oauth.getOAuthToken(function(err, token) { + expect(err).to.not.exist; + expect(token).to.equal('token'); + done(); + }); + }); + + it('should fail', function(done) { + identityStub.yields(); + + oauth.getOAuthToken(function(err, token) { + expect(err).to.exist; + expect(token).to.not.exist; + done(); + }); + }); + }); + + describe('queryEmailAddress', function() { + it('should work', function(done) { + googleApiStub.get.withArgs({ + uri: '/oauth2/v1/tokeninfo?access_token=token' + }).yields(null, { + email: 'asdf@example.com' + }); + + oauth.queryEmailAddress('token', function(err, emailAddress) { + expect(err).to.not.exist; + expect(emailAddress).to.equal('asdf@example.com'); + done(); + }); + }); + + it('should fail due to invalid token', function(done) { + oauth.queryEmailAddress('', function(err, emailAddress) { + expect(err).to.exist; + expect(emailAddress).to.not.exist; + done(); + }); + }); + + it('should fail due to error in rest api', function(done) { + googleApiStub.get.withArgs({ + uri: '/oauth2/v1/tokeninfo?access_token=token' + }).yields(new Error()); + + oauth.queryEmailAddress('token', function(err, emailAddress) { + expect(err).to.exist; + expect(emailAddress).to.not.exist; + done(); + }); + }); + }); + + }); +}); \ No newline at end of file diff --git a/test/new-unit/rest-dao-test.js b/test/new-unit/rest-dao-test.js index 2fe291a..57859dc 100644 --- a/test/new-unit/rest-dao-test.js +++ b/test/new-unit/rest-dao-test.js @@ -32,9 +32,7 @@ define(function(require) { it('should accept default base uri', function() { var baseUri = 'http://custom.com'; - restDao = new RestDAO({ - baseUri: baseUri - }); + restDao = new RestDAO(baseUri); expect(restDao).to.exist; expect(restDao._baseUri).to.equal(baseUri); });