[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
This commit is contained in:
Tankred Hase 2014-11-12 16:12:26 +01:00
parent 082cbf192b
commit cf1f60fbf9
20 changed files with 483 additions and 490 deletions

View File

@ -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'
]
},

View File

@ -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

View File

@ -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();
}
};

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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);
});
};

View File

@ -3,6 +3,8 @@
.page {
height: 100%;
overflow-y: auto;
// allow scrolling on iOS
-webkit-overflow-scrolling: touch;
// disable text selection
user-select: none;

View File

@ -6,17 +6,19 @@
<main class="page__main">
<h2 class="typo-title">Unlock mailbox</h2>
<p class="typo-paragraph">Please enter your passphrase to unlock the mailbox.</p>
<form class="form">
<!-- TODO add error messages -->
<!--<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<p class="form__error-message" ng-show="form.$invalid">Please fill out all required fields!</p>-->
<form class="form" name="form">
<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<div class="form__row">
<input class="input-text input-text--big" type="password" ng-model="passphrase" ng-change="change()"
ng-class="{'input-text--error':incorrect}" placeholder="Passphrase" tabindex="1" focus-me="true">
<input type="password" ng-model="passphrase"
class="input-text input-text--big" ng-class="{'input-text--error':incorrect}"
placeholder="Passphrase" tabindex="1" focus-me="true" required>
</div>
<div class="spinner-block" ng-show="busy">
<span class="spinner spinner--big"></span>
</div>
<div class="form__row">
<button class="btn btn--big" type="submit" wo-touch="confirmPassphrase()" ng-disabled="!buttonEnabled" tabindex="2">
<button class="btn btn--big" type="submit" ng-click="confirmPassphrase()" tabindex="2">
<svg role="presentation"><use xlink:href="#icon-decrypted" /></svg>
Unlock
</button>

View File

@ -12,13 +12,11 @@
</p>
<form class="form" name="form">
<!-- TODO: remove error dialog and use inline error messages -->
<!--<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<p class="form__error-message" ng-show="form.$invalid">Please fill out all required fields!</p>-->
<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<div class="form__row">
<label class="input-checkbox">
<span class="checkbox">
<input type="checkbox" ng-model="state.agree">
<input type="checkbox" ng-model="agree">
<span class="input-checkbox__box"><svg role="presentation"><use xlink:href="#icon-check" /></svg></span>
</span>
I agree to the Whiteout Networks <a href="https://whiteout.io/terms.html" target="_blank">Terms of Service</a> and have read the <a href="https://whiteout.io/privacy-service.html" target="_blank">Privacy Policy</a>
@ -27,17 +25,17 @@
<div class="form__row">
<label class="input-checkbox">
<span class="checkbox">
<input type="checkbox" ng-model="state.newsletter">
<input type="checkbox" ng-model="newsletter">
<span class="input-checkbox__box"><svg role="presentation"><use xlink:href="#icon-check" /></svg></span>
</span>
Stay up to date on Whiteout Networks products and important announcements.
</label>
</div>
<div class="form__row">
<button type="submit" wo-touch="generateKey()" class="btn" tabindex="3">Generate new key</button>
<button type="submit" ng-click="generateKey()" class="btn" tabindex="3">Generate new key</button>
</div>
<div class="form__row">
<button type="button" wo-touch="importKey()" class="btn btn--secondary">Import existing key</button>
<button type="button" ng-click="importKey()" class="btn btn--secondary">Import existing key</button>
</div>
</form>
</div>
@ -47,7 +45,7 @@
<p class="typo-paragraph">
Please stand by. This can take a while…
</p>
<div class="u-text-center">
<div class="spinner-block spinner-block--standalone">
<span class="spinner spinner--big"></span>
</div>
</div>

View File

@ -17,23 +17,19 @@
</p>
</fieldset>
<!-- TODO -->
<!--<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<p class="form__error-message" ng-show="form.$invalid">Please fill out all required fields!</p>-->
<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<div class="form__row">
<input class="input-file" type="file" accept=".asc" file-reader tabindex="1">
<input class="input-file" type="file" accept=".asc" file-reader tabindex="1" required>
</div>
<div class="form__row">
<input class="input-text" type="password" ng-model="passphrase"
ng-class="{'input-text--error':incorrect}" placeholder="Passphrase" tabindex="2">
</div>
<!-- TODO -->
<!--<div class="spinner-block" ng-show="busy">
<div class="spinner-block" ng-show="busy">
<span class="spinner spinner--big"></span>
</div>-->
</div>
<div class="form__row">
<button type="submit" wo-touch="confirmPassphrase()" class="btn" ng-disabled="!key" tabindex="3">Import</button>
<button type="submit" ng-click="confirmPassphrase()" class="btn" tabindex="3">Import</button>
</div>
</form>
<p class="typo-paragraph">

View File

@ -8,16 +8,18 @@
<div ng-show="step === 1">
<h2 class="typo-title">Key sync</h2>
<p class="typo-paragraph">We have sent you an email containing a recovery token. Please copy and paste the token below to download your key.</p>
<form class="form">
<!-- TODO add error messages -->
<!--<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<p class="form__error-message" ng-show="form.$invalid">Please fill out all required fields!</p>-->
<form class="form" name="tokenForm">
<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<div class="form__row">
<input type="text" class="input-text input-text--big" size="6" maxlength="6" ng-model="recoveryToken" placeholder="Token" focus-me="step === 1" pattern="([a-zA-Z0-9]*)">
<input type="text" class="input-text input-text--big" ng-class="{'input-text--error':incorrect}"
size="6" maxlength="6" ng-model="recoveryToken" placeholder="Token" focus-me="step === 1" pattern="([a-zA-Z0-9]*)" required>
</div>
<div class="spinner-block" ng-show="busy">
<span class="spinner spinner--big"></span>
</div>
<div class="form__row">
<button class="btn btn--big" wo-touch="goForward()">Confirm token</button>
<button class="btn btn--big" type="submit" ng-click="checkToken()">Confirm token</button>
</div>
</form>
@ -37,23 +39,24 @@
<div ng-show="step === 2">
<h2 class="typo-title">Key sync</h2>
<p class="typo-paragraph">Please enter the keychain code you wrote down during sync setup.</p>
<form class="form">
<!-- TODO add error messages -->
<!--<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<p class="form__error-message" ng-show="form.$invalid">Please fill out all required fields!</p>-->
<form class="form" name="codeForm">
<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<div class="form__row">
<div class="input-code">
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code0" focus-me="step === 2" focus-next ng-paste="handlePaste($event)"> -
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code1" focus-next> -
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code2" focus-next> -
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code3" focus-next> -
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code4" focus-next> -
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code5">
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code0" focus-me="step === 2" focus-next required ng-paste="handlePaste($event)"> -
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code1" focus-next required> -
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code2" focus-next required> -
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code3" focus-next required> -
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code4" focus-next required> -
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code5" required>
</div>
</div>
<div class="spinner-block" ng-show="busy">
<span class="spinner spinner--big"></span>
</div>
<div class="form__row">
<button class="btn" wo-touch="goForward()">Complete sync</button>
<button class="btn" type="submit" ng-click="checkCode()">Complete sync</button>
</div>
</form>
<p class="typo-paragraph">

View File

@ -108,7 +108,7 @@
<span class="spinner spinner--big"></span>
</div>
<div class="form__row">
<button type="submit" ng-disabled="form.$invalid" ng-click="test()" class="btn">Login</button>
<button type="submit" ng-click="test()" class="btn">Login</button>
</div>
</form>
</main>

View File

@ -12,10 +12,6 @@
</p>
<form class="form" name="form">
<! TODO use error messages -->
<!--<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<p class="form__error-message" ng-show="form.$invalid">Please fill out all required fields!</p>-->
<div class="form__row">
<input class="input-text" type="password" ng-model="oldPassphrase" placeholder="Current passphrase" tabindex="1" focus-me="true">
</div>

View File

@ -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;
});
});
});

View File

@ -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;
});
});
});

View File

@ -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.');
});
});
});

View File

@ -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) {

View File

@ -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);
}));
});
});