From cf1f60fbf90486404b7205ed43556acdfb0899c1 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 12 Nov 2014 16:12:26 +0100 Subject: [PATCH] [WO-649] clean up login pages * add spinners to all login pages * use inline error messages in all form instead of scope.onError * create newsletter service --- Gruntfile.js | 3 +- src/js/app.js | 1 + src/js/controller/login-existing.js | 31 ++- src/js/controller/login-initial.js | 108 ++++----- src/js/controller/login-new-device.js | 29 ++- .../controller/login-privatekey-download.js | 208 ++++++++++-------- src/js/service/mail-config.js | 2 +- src/js/service/newsletter.js | 45 ++++ src/sass/blocks/layout/_page.scss | 2 + src/tpl/login-existing.html | 16 +- src/tpl/login-initial.html | 14 +- src/tpl/login-new-device.html | 14 +- src/tpl/login-privatekey-download.html | 37 ++-- src/tpl/login-set-credentials.html | 2 +- src/tpl/set-passphrase.html | 4 - test/unit/login-existing-ctrl-test.js | 76 +++---- test/unit/login-initial-ctrl-test.js | 141 +++--------- test/unit/login-new-device-ctrl-test.js | 28 +-- .../login-privatekey-download-ctrl-test.js | 136 +++++------- test/unit/newsletter-service-test.js | 76 +++++++ 20 files changed, 483 insertions(+), 490 deletions(-) create mode 100644 src/js/service/newsletter.js create mode 100644 test/unit/newsletter-service-test.js diff --git a/Gruntfile.js b/Gruntfile.js index 1ff3f12..70eecfd 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -169,6 +169,8 @@ module.exports = function(grunt) { 'test/unit/lawnchair-dao-test.js', 'test/unit/keychain-dao-test.js', 'test/unit/devicestorage-dao-test.js', + 'test/unit/newsletter-service-test.js', + 'test/unit/mail-config-service-test.js', 'test/unit/dialog-ctrl-test.js', 'test/unit/add-account-ctrl-test.js', 'test/unit/create-account-ctrl-test.js', @@ -192,7 +194,6 @@ module.exports = function(grunt) { 'test/unit/invitation-dao-test.js', 'test/unit/update-handler-test.js', 'test/unit/connection-doctor-test.js', - 'test/unit/mail-config-service-test.js', 'test/main.js' ] }, diff --git a/src/js/app.js b/src/js/app.js index c188df3..d276e41 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -37,6 +37,7 @@ var DialogCtrl = require('./controller/dialog'), errorUtil = require('./util/error'), backButtonUtil = require('./util/backbutton-handler'); require('./directive/common'), +require('./service/newsletter'), require('./service/mail-config'); // init main angular module including dependencies diff --git a/src/js/controller/login-existing.js b/src/js/controller/login-existing.js index 006c2ec..24cf496 100644 --- a/src/js/controller/login-existing.js +++ b/src/js/controller/login-existing.js @@ -10,21 +10,16 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) { var emailDao = appController._emailDao; - $scope.buttonEnabled = true; - $scope.incorrect = false; - - $scope.change = function() { - $scope.incorrect = false; - }; - $scope.confirmPassphrase = function() { - if (!$scope.passphrase) { + if ($scope.form.$invalid) { + $scope.errMsg = 'Please fill out all required fields!'; return; } - // disable button once loggin has started - $scope.buttonEnabled = false; + $scope.busy = true; + $scope.errMsg = undefined; $scope.incorrect = false; + unlockCrypto(); }; @@ -32,7 +27,7 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) { var userId = emailDao._account.emailAddress; emailDao._keychain.getUserKeyPair(userId, function(err, keypair) { if (err) { - handleError(err); + displayError(err); return; } @@ -45,15 +40,14 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) { function onUnlock(err) { if (err) { - $scope.incorrect = true; - $scope.buttonEnabled = true; - $scope.$apply(); + displayError(err); return; } appController._auth.storeCredentials(function(err) { if (err) { - return $scope.onError(err); + displayError(err); + return; } $location.path('/desktop'); @@ -61,10 +55,11 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) { }); } - function handleError(err) { + function displayError(err) { + $scope.busy = false; $scope.incorrect = true; - $scope.buttonEnabled = true; - $scope.onError(err); + $scope.errMsg = err.errMsg || err.message; + $scope.$apply(); } }; diff --git a/src/js/controller/login-initial.js b/src/js/controller/login-initial.js index 231f74e..ced1bd1 100644 --- a/src/js/controller/login-initial.js +++ b/src/js/controller/login-initial.js @@ -2,20 +2,24 @@ var appController = require('../app-controller'); -var LoginInitialCtrl = function($scope, $location, $routeParams) { +var LoginInitialCtrl = function($scope, $location, $routeParams, newsletter) { if (!appController._emailDao && !$routeParams.dev) { $location.path('/'); // init app return; } - var emailDao = appController._emailDao, - states, termsMsg = 'You must accept the Terms of Service to continue.'; + if (appController._emailDao) { + var emailDao = appController._emailDao, + emailAddress = emailDao._account.emailAddress; + } + + var termsMsg = 'You must accept the Terms of Service to continue.', + states = { + IDLE: 1, + PROCESSING: 2, + DONE: 3 + }; - states = { - IDLE: 1, - PROCESSING: 2, - DONE: 3 - }; $scope.state.ui = states.IDLE; // initial state // @@ -26,15 +30,15 @@ var LoginInitialCtrl = function($scope, $location, $routeParams) { * Continue to key import screen */ $scope.importKey = function() { - if (!$scope.state.agree) { - $scope.onError({ - message: termsMsg - }); + if (!$scope.agree) { + displayError(new Error(termsMsg)); return; } + $scope.errMsg = undefined; + // sing up to newsletter - $scope.signUpToNewsletter(); + newsletter.signup(emailAddress, $scope.newsletter); // go to key import $location.path('/login-new-device'); }; @@ -43,77 +47,47 @@ var LoginInitialCtrl = function($scope, $location, $routeParams) { * Continue to keygen */ $scope.generateKey = function() { - if (!$scope.state.agree) { - $scope.onError({ - message: termsMsg - }); + if (!$scope.agree) { + displayError(new Error(termsMsg)); return; } + $scope.errMsg = undefined; + // sing up to newsletter - $scope.signUpToNewsletter(); + newsletter.signup(emailAddress, $scope.newsletter); // go to set keygen screen $scope.setState(states.PROCESSING); - setTimeout(function() { - emailDao.unlock({ - passphrase: undefined // generate key without passphrase - }, function(err) { + emailDao.unlock({ + passphrase: undefined // generate key without passphrase + }, function(err) { + if (err) { + displayError(err); + return; + } + + appController._auth.storeCredentials(function(err) { if (err) { - $scope.setState(states.IDLE); - $scope.onError(err); + displayError(err); return; } - appController._auth.storeCredentials(function(err) { - if (err) { - return $scope.onError(err); - } - - $location.path('/desktop'); - $scope.$apply(); - }); + $location.path('/desktop'); + $scope.$apply(); }); - }, 500); - }; - - /** - * [signUpToNewsletter description] - * @param {Function} callback (optional) - */ - $scope.signUpToNewsletter = function(callback) { - if (!$scope.state.newsletter) { - return; - } - - var address = emailDao._account.emailAddress; - var uri = 'https://whiteout.us8.list-manage.com/subscribe/post?u=52ea5a9e1be9e1d194f184158&id=6538e8f09f'; - - var formData = new FormData(); - formData.append('EMAIL', address); - formData.append('b_52ea5a9e1be9e1d194f184158_6538e8f09f', ''); - - var xhr = new XMLHttpRequest(); - xhr.open('post', uri, true); - - xhr.onload = function() { - if (callback) { - callback(null, xhr); - } - }; - - xhr.onerror = function(err) { - if (callback) { - callback(err); - } - }; - - xhr.send(formData); + }); }; $scope.setState = function(state) { $scope.state.ui = state; }; + + function displayError(err) { + $scope.setState(states.IDLE); + $scope.errMsg = err.errMsg || err.message; + $scope.$apply(); + } }; module.exports = LoginInitialCtrl; \ No newline at end of file diff --git a/src/js/controller/login-new-device.js b/src/js/controller/login-new-device.js index c9f16d1..fc24b17 100644 --- a/src/js/controller/login-new-device.js +++ b/src/js/controller/login-new-device.js @@ -14,7 +14,15 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) { $scope.incorrect = false; $scope.confirmPassphrase = function() { + if ($scope.form.$invalid || !$scope.key) { + $scope.errMsg = 'Please fill out all required fields!'; + return; + } + + $scope.busy = true; + $scope.errMsg = undefined; // reset error msg $scope.incorrect = false; + unlockCrypto(); }; @@ -23,7 +31,7 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) { // check if user already has a public key on the key server emailDao._keychain.getUserKeyPair(userId, function(err, keypair) { if (err) { - $scope.onError(err); + $scope.displayError(err); return; } @@ -34,7 +42,7 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) { try { $scope.key.publicKeyArmored = pgp.extractPublicKey($scope.key.privateKeyArmored); } catch (e) { - $scope.onError(new Error('Error parsing public key from private key!')); + $scope.displayError(new Error('Error reading PGP key!')); return; } } @@ -45,7 +53,7 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) { privKeyParams = pgp.getKeyParams($scope.key.privateKeyArmored); pubKeyParams = pgp.getKeyParams($scope.key.publicKeyArmored); } catch (e) { - $scope.onError(new Error('Error reading key params!')); + $scope.displayError(new Error('Error reading key params!')); return; } @@ -74,7 +82,7 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) { }, function(err) { if (err) { $scope.incorrect = true; - $scope.onError(err); + $scope.displayError(err); return; } @@ -85,19 +93,26 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) { function onUnlock(err) { if (err) { - $scope.onError(err); + $scope.displayError(err); return; } appController._auth.storeCredentials(function(err) { if (err) { - return $scope.onError(err); + $scope.displayError(err); + return; } $location.path('/desktop'); $scope.$apply(); }); } + + $scope.displayError = function(err) { + $scope.busy = false; + $scope.errMsg = err.errMsg || err.message; + $scope.$apply(); + }; }; var ngModule = angular.module('login-new-device', []); @@ -117,7 +132,7 @@ ngModule.directive('fileReader', function() { keyParts; if (index === -1) { - scope.onError(new Error('Error parsing private PGP key block!')); + scope.displayError(new Error('Error parsing private PGP key block!')); return; } diff --git a/src/js/controller/login-privatekey-download.js b/src/js/controller/login-privatekey-download.js index 3e7aec5..e45106c 100644 --- a/src/js/controller/login-privatekey-download.js +++ b/src/js/controller/login-privatekey-download.js @@ -8,12 +8,116 @@ var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams) { return; } - var keychain = appController._keychain, - emailDao = appController._emailDao, - userId = emailDao._account.emailAddress; + if (appController._emailDao) { + var keychain = appController._keychain, + emailDao = appController._emailDao, + userId = emailDao._account.emailAddress; + } $scope.step = 1; + // + // Token + // + + $scope.checkToken = function() { + if ($scope.tokenForm.$invalid) { + $scope.errMsg = 'Please enter a valid recovery token!'; + return; + } + + $scope.busy = true; + $scope.errMsg = undefined; + + $scope.verifyRecoveryToken(function() { + $scope.busy = false; + $scope.errMsg = undefined; + $scope.step++; + $scope.$apply(); + }); + }; + + $scope.verifyRecoveryToken = function(callback) { + keychain.getUserKeyPair(userId, function(err, keypair) { + if (err) { + displayError(err); + return; + } + + // remember for storage later + $scope.cachedKeypair = keypair; + + keychain.downloadPrivateKey({ + userId: userId, + keyId: keypair.publicKey._id, + recoveryToken: $scope.recoveryToken.toUpperCase() + }, function(err, encryptedPrivateKey) { + if (err) { + displayError(err); + return; + } + + $scope.encryptedPrivateKey = encryptedPrivateKey; + callback(); + }); + }); + }; + + // + // Keychain code + // + + $scope.checkCode = function() { + if ($scope.codeForm.$invalid) { + $scope.errMsg = 'Please fill out all required fields!'; + return; + } + + $scope.busy = true; + $scope.errMsg = undefined; + + $scope.decryptAndStorePrivateKeyLocally(); + }; + + $scope.decryptAndStorePrivateKeyLocally = function() { + var inputCode = '' + $scope.code0 + $scope.code1 + $scope.code2 + $scope.code3 + $scope.code4 + $scope.code5; + + var options = $scope.encryptedPrivateKey; + options.code = inputCode.toUpperCase(); + + keychain.decryptAndStorePrivateKeyLocally(options, function(err, privateKey) { + if (err) { + displayError(err); + return; + } + + // add private key to cached keypair object + $scope.cachedKeypair.privateKey = privateKey; + + // try empty passphrase + emailDao.unlock({ + keypair: $scope.cachedKeypair, + passphrase: undefined + }, function(err) { + if (err) { + // go to passphrase login screen + $scope.goTo('/login-existing'); + return; + } + + // passphrase is corrent ... go to main app + appController._auth.storeCredentials(function(err) { + if (err) { + displayError(err); + return; + } + + $scope.goTo('/desktop'); + }); + }); + }); + }; + $scope.handlePaste = function(event) { var evt = event; if (evt.originalEvent) { @@ -34,99 +138,21 @@ var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams) { $scope.code5 = value.slice(20, 24); }; - $scope.verifyRecoveryToken = function(callback) { - if (!$scope.recoveryToken) { - $scope.onError(new Error('Please set the recovery token!')); - return; - } - - keychain.getUserKeyPair(userId, function(err, keypair) { - if (err) { - $scope.onError(err); - return; - } - - // remember for storage later - $scope.cachedKeypair = keypair; - - keychain.downloadPrivateKey({ - userId: userId, - keyId: keypair.publicKey._id, - recoveryToken: $scope.recoveryToken.toUpperCase() - }, function(err, encryptedPrivateKey) { - if (err) { - $scope.onError(err); - return; - } - - $scope.encryptedPrivateKey = encryptedPrivateKey; - callback(); - }); - }); - }; - - $scope.decryptAndStorePrivateKeyLocally = function() { - var inputCode = '' + $scope.code0 + $scope.code1 + $scope.code2 + $scope.code3 + $scope.code4 + $scope.code5; - - if (!inputCode) { - $scope.onError(new Error('Please enter the keychain code!')); - return; - } - - var options = $scope.encryptedPrivateKey; - options.code = inputCode.toUpperCase(); - - keychain.decryptAndStorePrivateKeyLocally(options, function(err, privateKey) { - if (err) { - $scope.onError(err); - return; - } - - // add private key to cached keypair object - $scope.cachedKeypair.privateKey = privateKey; - - // try empty passphrase - emailDao.unlock({ - keypair: $scope.cachedKeypair, - passphrase: undefined - }, function(err) { - if (err) { - // go to passphrase login screen - $scope.goTo('/login-existing'); - return; - } - - // passphrase is corrent ... go to main app - appController._auth.storeCredentials(function(err) { - if (err) { - return $scope.onError(err); - } - - $scope.goTo('/desktop'); - }); - }); - }); - }; - - $scope.goForward = function() { - if ($scope.step === 1) { - $scope.verifyRecoveryToken(function() { - $scope.step++; - $scope.$apply(); - }); - return; - } - - if ($scope.step === 2) { - $scope.decryptAndStorePrivateKeyLocally(); - return; - } - }; + // + // helper functions + // $scope.goTo = function(location) { $location.path(location); $scope.$apply(); }; + + function displayError(err) { + $scope.busy = false; + $scope.incorrect = true; + $scope.errMsg = err.errMsg || err.message; + $scope.$apply(); + } }; module.exports = LoginPrivateKeyDownloadCtrl; \ No newline at end of file diff --git a/src/js/service/mail-config.js b/src/js/service/mail-config.js index 3b9eb38..df7eda9 100644 --- a/src/js/service/mail-config.js +++ b/src/js/service/mail-config.js @@ -1,6 +1,6 @@ 'use strict'; -var ngModule = angular.module('woServices', []); +var ngModule = angular.module('woServices'); ngModule.service('mailConfig', MailConfig); var cfg = require('../app-config').config; diff --git a/src/js/service/newsletter.js b/src/js/service/newsletter.js new file mode 100644 index 0000000..a3c2c4c --- /dev/null +++ b/src/js/service/newsletter.js @@ -0,0 +1,45 @@ +'use strict'; + +var ngModule = angular.module('woServices', []); +ngModule.service('newsletter', Newsletter); + +function Newsletter($q) { + this._q = $q; +} + +/** + * Sign up to the whiteout newsletter + */ +Newsletter.prototype.signup = function(emailAddress, agree) { + return this._q(function(resolve, reject) { + // validate email address + if (emailAddress.indexOf('@') < 0) { + reject(new Error('Invalid email address!')); + return; + } + + if (!agree) { + // don't sign up if the user has not agreed + resolve(false); + return; + } + + var formData = new FormData(); + formData.append('EMAIL', emailAddress); + formData.append('b_52ea5a9e1be9e1d194f184158_6538e8f09f', ''); + + var uri = 'https://whiteout.us8.list-manage.com/subscribe/post?u=52ea5a9e1be9e1d194f184158&id=6538e8f09f'; + var xhr = new XMLHttpRequest(); + xhr.open('post', uri, true); + + xhr.onload = function() { + resolve(xhr); + }; + + xhr.onerror = function(err) { + reject(err); + }; + + xhr.send(formData); + }); +}; \ No newline at end of file diff --git a/src/sass/blocks/layout/_page.scss b/src/sass/blocks/layout/_page.scss index 92d99f2..1c72d8d 100644 --- a/src/sass/blocks/layout/_page.scss +++ b/src/sass/blocks/layout/_page.scss @@ -3,6 +3,8 @@ .page { height: 100%; overflow-y: auto; + // allow scrolling on iOS + -webkit-overflow-scrolling: touch; // disable text selection user-select: none; diff --git a/src/tpl/login-existing.html b/src/tpl/login-existing.html index 3c9abf6..5f543db 100644 --- a/src/tpl/login-existing.html +++ b/src/tpl/login-existing.html @@ -6,17 +6,19 @@

Unlock mailbox

Please enter your passphrase to unlock the mailbox.

-
- - + +

{{errMsg}}

- + +
+
+
- diff --git a/src/tpl/login-initial.html b/src/tpl/login-initial.html index cb7217b..b2bc63c 100644 --- a/src/tpl/login-initial.html +++ b/src/tpl/login-initial.html @@ -12,13 +12,11 @@

- - +

{{errMsg}}

@@ -47,7 +45,7 @@

Please stand by. This can take a while…

-
+
diff --git a/src/tpl/login-new-device.html b/src/tpl/login-new-device.html index 6377146..a10e66f 100644 --- a/src/tpl/login-new-device.html +++ b/src/tpl/login-new-device.html @@ -17,23 +17,19 @@

- - - +

{{errMsg}}

- +
- - +
- +

diff --git a/src/tpl/login-privatekey-download.html b/src/tpl/login-privatekey-download.html index 88fea03..c55ed18 100644 --- a/src/tpl/login-privatekey-download.html +++ b/src/tpl/login-privatekey-download.html @@ -8,16 +8,18 @@

Key sync

We have sent you an email containing a recovery token. Please copy and paste the token below to download your key.

-
- - + +

{{errMsg}}

- + +
+
+
- +
@@ -37,23 +39,24 @@

Key sync

Please enter the keychain code you wrote down during sync setup.

-
- - + +

{{errMsg}}

- - - - - - - - - - - + - + - + - + - + - +
+
+ +
- +

diff --git a/src/tpl/login-set-credentials.html b/src/tpl/login-set-credentials.html index 61791f6..8995013 100644 --- a/src/tpl/login-set-credentials.html +++ b/src/tpl/login-set-credentials.html @@ -108,7 +108,7 @@

- +
diff --git a/src/tpl/set-passphrase.html b/src/tpl/set-passphrase.html index 2eee0e5..42195de 100644 --- a/src/tpl/set-passphrase.html +++ b/src/tpl/set-passphrase.html @@ -12,10 +12,6 @@

- - -
diff --git a/test/unit/login-existing-ctrl-test.js b/test/unit/login-existing-ctrl-test.js index 521b954..7dc2e19 100644 --- a/test/unit/login-existing-ctrl-test.js +++ b/test/unit/login-existing-ctrl-test.js @@ -35,6 +35,7 @@ describe('Login (existing user) Controller unit test', function() { location = $location; scope = $rootScope.$new(); scope.state = {}; + scope.form = {}; ctrl = $controller(LoginExistingCtrl, { $scope: scope, $routeParams: {} @@ -50,64 +51,37 @@ describe('Login (existing user) Controller unit test', function() { describe('initial state', function() { it('should be well defined', function() { - expect(scope.buttonEnabled).to.be.true; - expect(scope.incorrect).to.be.false; - expect(scope.change).to.exist; - expect(scope.confirmPassphrase).to.exist; + expect(scope.incorrect).to.be.undefined; }); }); - describe('functionality', function() { - describe('change', function() { - it('should set incorrect to false', function() { - scope.incorrect = true; + describe('confirm passphrase', function() { + it('should unlock crypto and start', function() { + var keypair = {}, + pathSpy = sinon.spy(location, 'path'); + scope.passphrase = passphrase; + keychainMock.getUserKeyPair.withArgs(emailAddress).yields(null, keypair); + emailDaoMock.unlock.withArgs({ + keypair: keypair, + passphrase: passphrase + }).yields(); + authMock.storeCredentials.yields(); - scope.change(); - expect(scope.incorrect).to.be.false; - }); + scope.confirmPassphrase(); + + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; + expect(emailDaoMock.unlock.calledOnce).to.be.true; + expect(pathSpy.calledOnce).to.be.true; + expect(pathSpy.calledWith('/desktop')).to.be.true; }); - describe('confirm passphrase', function() { - it('should unlock crypto and start', function() { - var keypair = {}, - pathSpy = sinon.spy(location, 'path'); - scope.passphrase = passphrase; - keychainMock.getUserKeyPair.withArgs(emailAddress).yields(null, keypair); - emailDaoMock.unlock.withArgs({ - keypair: keypair, - passphrase: passphrase - }).yields(); - authMock.storeCredentials.yields(); + it('should not work when keypair unavailable', function() { + scope.passphrase = passphrase; + keychainMock.getUserKeyPair.withArgs(emailAddress).yields(new Error('asd')); - - scope.confirmPassphrase(); - - expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; - expect(emailDaoMock.unlock.calledOnce).to.be.true; - expect(pathSpy.calledOnce).to.be.true; - expect(pathSpy.calledWith('/desktop')).to.be.true; - }); - - it('should not do anything without passphrase', function() { - var pathSpy = sinon.spy(location, 'path'); - scope.passphrase = ''; - - scope.confirmPassphrase(); - expect(pathSpy.callCount).to.equal(0); - }); - - it('should not work when keypair unavailable', function(done) { - scope.passphrase = passphrase; - keychainMock.getUserKeyPair.withArgs(emailAddress).yields(new Error('asd')); - - scope.onError = function(err) { - expect(err.message).to.equal('asd'); - expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; - done(); - }; - - scope.confirmPassphrase(); - }); + scope.confirmPassphrase(); + expect(scope.errMsg).to.exist; + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; }); }); }); \ No newline at end of file diff --git a/test/unit/login-initial-ctrl-test.js b/test/unit/login-initial-ctrl-test.js index 170db85..5777650 100644 --- a/test/unit/login-initial-ctrl-test.js +++ b/test/unit/login-initial-ctrl-test.js @@ -9,7 +9,7 @@ var Auth = require('../../src/js/bo/auth'), describe('Login (initial user) Controller unit test', function() { var scope, ctrl, location, origEmailDao, emailDaoMock, - origAuth, authMock, + origAuth, authMock, newsletterStub, emailAddress = 'fred@foo.com', keyId, expectedKeyId, cryptoMock; @@ -31,17 +31,19 @@ describe('Login (initial user) Controller unit test', function() { emailAddress: emailAddress, }; - angular.module('logininitialtest', []); + angular.module('logininitialtest', ['woServices']); mocks.module('logininitialtest'); - mocks.inject(function($rootScope, $controller, $location) { + mocks.inject(function($rootScope, $controller, $location, newsletter) { scope = $rootScope.$new(); location = $location; + newsletterStub = sinon.stub(newsletter, 'signup'); scope.state = { ui: {} }; ctrl = $controller(LoginInitialCtrl, { $scope: scope, - $routeParams: {} + $routeParams: {}, + newsletter: newsletter }); }); }); @@ -58,140 +60,65 @@ describe('Login (initial user) Controller unit test', function() { }); }); - describe('signUpToNewsletter', function() { - var xhrMock, requests; - - beforeEach(function() { - xhrMock = sinon.useFakeXMLHttpRequest(); - requests = []; - - xhrMock.onCreate = function(xhr) { - requests.push(xhr); - }; - }); - - afterEach(function() { - xhrMock.restore(); - }); - - it('should not signup', function() { - scope.state.newsletter = false; - - scope.signUpToNewsletter(); - expect(requests.length).to.equal(0); - }); - - it('should fail', function(done) { - scope.state.newsletter = true; - - scope.signUpToNewsletter(function(err, xhr) { - expect(err).to.exist; - expect(xhr).to.not.exist; - done(); - }); - - expect(requests.length).to.equal(1); - requests[0].onerror('err'); - }); - - it('should work without callback', function() { - scope.state.newsletter = true; - - scope.signUpToNewsletter(); - - expect(requests.length).to.equal(1); - requests[0].respond(200, { - "Content-Type": "text/plain" - }, 'foobar!'); - }); - }); - describe('go to import key', function() { - var signUpToNewsletterStub; - beforeEach(function() { - signUpToNewsletterStub = sinon.stub(scope, 'signUpToNewsletter'); - }); - afterEach(function() { - signUpToNewsletterStub.restore(); - }); - - it('should not continue if terms are not accepted', function(done) { - scope.state.agree = undefined; - - scope.onError = function(err) { - expect(err.message).to.contain('Terms'); - expect(signUpToNewsletterStub.called).to.be.false; - done(); - }; + it('should not continue if terms are not accepted', function() { + scope.agree = undefined; scope.importKey(); + + expect(scope.errMsg).to.contain('Terms'); + expect(newsletterStub.called).to.be.false; }); it('should work', function() { - scope.state.agree = true; + scope.agree = true; scope.importKey(); - expect(signUpToNewsletterStub.calledOnce).to.be.true; + expect(newsletterStub.calledOnce).to.be.true; expect(location.$$path).to.equal('/login-new-device'); }); }); describe('generate key', function() { - var signUpToNewsletterStub; - beforeEach(function() { - signUpToNewsletterStub = sinon.stub(scope, 'signUpToNewsletter'); - }); - afterEach(function() { - signUpToNewsletterStub.restore(); - }); - - it('should not continue if terms are not accepted', function(done) { - scope.state.agree = undefined; - - scope.onError = function(err) { - expect(err.message).to.contain('Terms'); - expect(scope.state.ui).to.equal(1); - expect(signUpToNewsletterStub.called).to.be.false; - done(); - }; + it('should not continue if terms are not accepted', function() { + scope.agree = undefined; scope.generateKey(); + + expect(scope.errMsg).to.contain('Terms'); + expect(scope.state.ui).to.equal(1); + expect(newsletterStub.called).to.be.false; }); - it('should fail due to error in emailDao.unlock', function(done) { - scope.state.agree = true; + it('should fail due to error in emailDao.unlock', function() { + scope.agree = true; emailDaoMock.unlock.withArgs({ passphrase: undefined - }).yields(new Error()); + }).yields(new Error('asdf')); authMock.storeCredentials.yields(); - scope.onError = function(err) { - expect(err).to.exist; - expect(scope.state.ui).to.equal(1); - expect(signUpToNewsletterStub.called).to.be.true; - done(); - }; - scope.generateKey(); - expect(scope.state.ui).to.equal(2); + + expect(scope.errMsg).to.exist; + expect(scope.state.ui).to.equal(1); + expect(newsletterStub.called).to.be.true; }); - it('should unlock crypto', function(done) { - scope.state.agree = true; + it('should unlock crypto', function() { + scope.agree = true; emailDaoMock.unlock.withArgs({ passphrase: undefined }).yields(); authMock.storeCredentials.yields(); - scope.$apply = function() { - expect(scope.state.ui).to.equal(2); - expect(location.$$path).to.equal('/desktop'); - expect(emailDaoMock.unlock.calledOnce).to.be.true; - done(); - }; - scope.generateKey(); + + expect(scope.errMsg).to.not.exist; + expect(scope.state.ui).to.equal(2); + expect(newsletterStub.called).to.be.true; + expect(location.$$path).to.equal('/desktop'); + expect(emailDaoMock.unlock.calledOnce).to.be.true; }); }); }); \ No newline at end of file diff --git a/test/unit/login-new-device-ctrl-test.js b/test/unit/login-new-device-ctrl-test.js index 2115bb2..97e1ad4 100644 --- a/test/unit/login-new-device-ctrl-test.js +++ b/test/unit/login-new-device-ctrl-test.js @@ -37,6 +37,7 @@ describe('Login (new device) Controller unit test', function() { scope.state = { ui: {} }; + scope.form = {}; ctrl = $controller(LoginNewDeviceCtrl, { $scope: scope, $routeParams: {} @@ -103,7 +104,7 @@ describe('Login (new device) Controller unit test', function() { expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; }); - it('should not work when keypair upload fails', function(done) { + it('should not work when keypair upload fails', function() { scope.passphrase = passphrase; scope.key = { privateKeyArmored: 'b' @@ -123,19 +124,15 @@ describe('Login (new device) Controller unit test', function() { errMsg: 'yo mamma.' }); - scope.onError = function(err) { - expect(err.errMsg).to.equal('yo mamma.'); - done(); - }; - scope.confirmPassphrase(); expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; expect(emailDaoMock.unlock.calledOnce).to.be.true; expect(keychainMock.putUserKeyPair.calledOnce).to.be.true; + expect(scope.errMsg).to.equal('yo mamma.'); }); - it('should not work when unlock fails', function(done) { + it('should not work when unlock fails', function() { scope.passphrase = passphrase; scope.key = { privateKeyArmored: 'b' @@ -154,33 +151,28 @@ describe('Login (new device) Controller unit test', function() { errMsg: 'yo mamma.' }); - scope.onError = function(err) { - expect(err.errMsg).to.equal('yo mamma.'); - done(); - }; - scope.confirmPassphrase(); expect(scope.incorrect).to.be.true; expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; expect(emailDaoMock.unlock.calledOnce).to.be.true; + expect(scope.errMsg).to.equal('yo mamma.'); }); - it('should not work when keypair retrieval', function(done) { + it('should not work when keypair retrieval', function() { scope.passphrase = passphrase; + scope.key = { + privateKeyArmored: 'b' + }; keychainMock.getUserKeyPair.withArgs(emailAddress).yields({ errMsg: 'yo mamma.' }); - scope.onError = function(err) { - expect(err.errMsg).to.equal('yo mamma.'); - done(); - }; - scope.confirmPassphrase(); expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; + expect(scope.errMsg).to.equal('yo mamma.'); }); }); }); \ No newline at end of file diff --git a/test/unit/login-privatekey-download-ctrl-test.js b/test/unit/login-privatekey-download-ctrl-test.js index b8986f3..e20c54c 100644 --- a/test/unit/login-privatekey-download-ctrl-test.js +++ b/test/unit/login-privatekey-download-ctrl-test.js @@ -33,6 +33,8 @@ describe('Login Private Key Download Controller unit test', function() { mocks.inject(function($controller, $rootScope) { scope = $rootScope.$new(); scope.state = {}; + scope.tokenForm = {}; + scope.codeForm = {}; ctrl = $controller(LoginPrivateKeyDownloadCtrl, { $location: location, $scope: scope, @@ -55,6 +57,35 @@ describe('Login Private Key Download Controller unit test', function() { }); }); + describe('checkToken', function() { + var verifyRecoveryTokenStub; + + beforeEach(function() { + verifyRecoveryTokenStub = sinon.stub(scope, 'verifyRecoveryToken'); + }); + afterEach(function() { + verifyRecoveryTokenStub.restore(); + }); + + it('should fail for empty recovery token', function() { + scope.tokenForm.$invalid = true; + + scope.checkToken(); + + expect(verifyRecoveryTokenStub.calledOnce).to.be.false; + expect(scope.errMsg).to.exist; + }); + + it('should work', function() { + verifyRecoveryTokenStub.yields(); + + scope.checkToken(); + + expect(verifyRecoveryTokenStub.calledOnce).to.be.true; + expect(scope.step).to.equal(2); + }); + }); + describe('verifyRecoveryToken', function() { var testKeypair = { publicKey: { @@ -62,53 +93,35 @@ describe('Login Private Key Download Controller unit test', function() { } }; - it('should fail for empty recovery token', function(done) { - scope.onError = function(err) { - expect(err).to.exist; - done(); - }; + it('should fail in keychain.getUserKeyPair', function() { + keychainMock.getUserKeyPair.yields(new Error('asdf')); - scope.recoveryToken = undefined; scope.verifyRecoveryToken(); + + expect(scope.errMsg).to.exist; + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; }); - it('should fail in keychain.getUserKeyPair', function(done) { - keychainMock.getUserKeyPair.yields(42); - - scope.onError = function(err) { - expect(err).to.exist; - expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; - done(); - }; - - scope.recoveryToken = 'token'; - scope.verifyRecoveryToken(); - }); - - it('should fail in keychain.downloadPrivateKey', function(done) { + it('should fail in keychain.downloadPrivateKey', function() { keychainMock.getUserKeyPair.yields(null, testKeypair); - keychainMock.downloadPrivateKey.yields(42); - - scope.onError = function(err) { - expect(err).to.exist; - expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; - expect(keychainMock.downloadPrivateKey.calledOnce).to.be.true; - done(); - }; - + keychainMock.downloadPrivateKey.yields(new Error('asdf')); scope.recoveryToken = 'token'; + scope.verifyRecoveryToken(); + + expect(scope.errMsg).to.exist; + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; + expect(keychainMock.downloadPrivateKey.calledOnce).to.be.true; }); - it('should work', function(done) { + it('should work', function() { keychainMock.getUserKeyPair.yields(null, testKeypair); keychainMock.downloadPrivateKey.yields(null, 'encryptedPrivateKey'); - scope.recoveryToken = 'token'; - scope.verifyRecoveryToken(function() { - expect(scope.encryptedPrivateKey).to.equal('encryptedPrivateKey'); - done(); - }); + + scope.verifyRecoveryToken(function() {}); + + expect(scope.encryptedPrivateKey).to.equal('encryptedPrivateKey'); }); }); @@ -151,39 +164,20 @@ describe('Login Private Key Download Controller unit test', function() { }; }); - it('should fail on empty code', function(done) { - scope.code0 = ''; - scope.code1 = ''; - scope.code2 = ''; - scope.code3 = ''; - scope.code4 = ''; - scope.code5 = ''; - - scope.onError = function(err) { - expect(err).to.exist; - done(); - }; + it('should fail on decryptAndStorePrivateKeyLocally', function() { + keychainMock.decryptAndStorePrivateKeyLocally.yields(new Error('asdf')); scope.decryptAndStorePrivateKeyLocally(); - }); - it('should fail on decryptAndStorePrivateKeyLocally', function(done) { - keychainMock.decryptAndStorePrivateKeyLocally.yields(42); - - scope.onError = function(err) { - expect(err).to.exist; - expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true; - done(); - }; - - scope.decryptAndStorePrivateKeyLocally(); + expect(scope.errMsg).to.exist; + expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true; }); it('should goto /login-existing on emailDao.unlock fail', function(done) { keychainMock.decryptAndStorePrivateKeyLocally.yields(null, { encryptedKey: 'keyArmored' }); - emailDaoMock.unlock.yields(42); + emailDaoMock.unlock.yields(new Error('asdf')); scope.goTo = function(location) { expect(location).to.equal('/login-existing'); @@ -213,30 +207,6 @@ describe('Login Private Key Download Controller unit test', function() { }); }); - describe('goForward', function() { - it('should work in step 1', function() { - var verifyRecoveryTokenStub = sinon.stub(scope, 'verifyRecoveryToken'); - verifyRecoveryTokenStub.yields(); - scope.step = 1; - - scope.goForward(); - - expect(verifyRecoveryTokenStub.calledOnce).to.be.true; - expect(scope.step).to.equal(2); - verifyRecoveryTokenStub.restore(); - }); - it('should work in step 2', function() { - var decryptAndStorePrivateKeyLocallyStub = sinon.stub(scope, 'decryptAndStorePrivateKeyLocally'); - decryptAndStorePrivateKeyLocallyStub.returns(); - scope.step = 2; - - scope.goForward(); - - expect(decryptAndStorePrivateKeyLocallyStub.calledOnce).to.be.true; - decryptAndStorePrivateKeyLocallyStub.restore(); - }); - }); - describe('goTo', function() { it('should work', function(done) { mocks.inject(function($controller, $rootScope, $location) { diff --git a/test/unit/newsletter-service-test.js b/test/unit/newsletter-service-test.js new file mode 100644 index 0000000..38bbd2f --- /dev/null +++ b/test/unit/newsletter-service-test.js @@ -0,0 +1,76 @@ +'use strict'; + +var mocks = angular.mock; +require('../../src/js/service/newsletter'); + +describe('Newsletter Service unit test', function() { + var newsletter; + + beforeEach(function() { + angular.module('newsletter-test', ['woServices']); + mocks.module('newsletter-test'); + mocks.inject(function($injector) { + newsletter = $injector.get('newsletter'); + }); + }); + + afterEach(function() {}); + + describe('signup', function() { + var xhrMock, requests; + + beforeEach(function() { + xhrMock = sinon.useFakeXMLHttpRequest(); + requests = []; + + xhrMock.onCreate = function(xhr) { + requests.push(xhr); + }; + }); + + afterEach(function() { + xhrMock.restore(); + }); + + it('should not signup if user has not agreed', inject(function($rootScope) { + newsletter.signup('text@example.com', false).then(function(result) { + expect(result).to.be.false; + }); + + $rootScope.$apply(); + expect(requests.length).to.equal(0); + })); + + it('should not signup due to invalid email address', inject(function($rootScope) { + newsletter.signup('textexample.com', true).catch(function(err) { + expect(err.message).to.contain('Invalid'); + }); + + $rootScope.$apply(); + expect(requests.length).to.equal(0); + })); + + it('should fail', inject(function($rootScope) { + newsletter.signup('text@example.com', true).catch(function(err) { + expect(err).to.exist; + }); + + requests[0].onerror('err'); + $rootScope.$apply(); + expect(requests.length).to.equal(1); + })); + + it('should work', inject(function($rootScope) { + newsletter.signup('text@example.com', true).then(function(result) { + expect(result).to.exist; + }); + + requests[0].respond(200, { + "Content-Type": "text/plain" + }, 'foobar!'); + $rootScope.$apply(); + expect(requests.length).to.equal(1); + })); + }); + +}); \ No newline at end of file