1
0
mirror of https://github.com/moparisthebest/mail synced 2024-11-29 12:22:22 -05:00

[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/lawnchair-dao-test.js',
'test/unit/keychain-dao-test.js', 'test/unit/keychain-dao-test.js',
'test/unit/devicestorage-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/dialog-ctrl-test.js',
'test/unit/add-account-ctrl-test.js', 'test/unit/add-account-ctrl-test.js',
'test/unit/create-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/invitation-dao-test.js',
'test/unit/update-handler-test.js', 'test/unit/update-handler-test.js',
'test/unit/connection-doctor-test.js', 'test/unit/connection-doctor-test.js',
'test/unit/mail-config-service-test.js',
'test/main.js' 'test/main.js'
] ]
}, },

View File

@ -37,6 +37,7 @@ var DialogCtrl = require('./controller/dialog'),
errorUtil = require('./util/error'), errorUtil = require('./util/error'),
backButtonUtil = require('./util/backbutton-handler'); backButtonUtil = require('./util/backbutton-handler');
require('./directive/common'), require('./directive/common'),
require('./service/newsletter'),
require('./service/mail-config'); require('./service/mail-config');
// init main angular module including dependencies // init main angular module including dependencies

View File

@ -10,21 +10,16 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) {
var emailDao = appController._emailDao; var emailDao = appController._emailDao;
$scope.buttonEnabled = true;
$scope.incorrect = false;
$scope.change = function() {
$scope.incorrect = false;
};
$scope.confirmPassphrase = function() { $scope.confirmPassphrase = function() {
if (!$scope.passphrase) { if ($scope.form.$invalid) {
$scope.errMsg = 'Please fill out all required fields!';
return; return;
} }
// disable button once loggin has started $scope.busy = true;
$scope.buttonEnabled = false; $scope.errMsg = undefined;
$scope.incorrect = false; $scope.incorrect = false;
unlockCrypto(); unlockCrypto();
}; };
@ -32,7 +27,7 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) {
var userId = emailDao._account.emailAddress; var userId = emailDao._account.emailAddress;
emailDao._keychain.getUserKeyPair(userId, function(err, keypair) { emailDao._keychain.getUserKeyPair(userId, function(err, keypair) {
if (err) { if (err) {
handleError(err); displayError(err);
return; return;
} }
@ -45,15 +40,14 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) {
function onUnlock(err) { function onUnlock(err) {
if (err) { if (err) {
$scope.incorrect = true; displayError(err);
$scope.buttonEnabled = true;
$scope.$apply();
return; return;
} }
appController._auth.storeCredentials(function(err) { appController._auth.storeCredentials(function(err) {
if (err) { if (err) {
return $scope.onError(err); displayError(err);
return;
} }
$location.path('/desktop'); $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.incorrect = true;
$scope.buttonEnabled = true; $scope.errMsg = err.errMsg || err.message;
$scope.onError(err); $scope.$apply();
} }
}; };

View File

@ -2,20 +2,24 @@
var appController = require('../app-controller'); var appController = require('../app-controller');
var LoginInitialCtrl = function($scope, $location, $routeParams) { var LoginInitialCtrl = function($scope, $location, $routeParams, newsletter) {
if (!appController._emailDao && !$routeParams.dev) { if (!appController._emailDao && !$routeParams.dev) {
$location.path('/'); // init app $location.path('/'); // init app
return; return;
} }
if (appController._emailDao) {
var emailDao = appController._emailDao, var emailDao = appController._emailDao,
states, termsMsg = 'You must accept the Terms of Service to continue.'; emailAddress = emailDao._account.emailAddress;
}
var termsMsg = 'You must accept the Terms of Service to continue.',
states = { states = {
IDLE: 1, IDLE: 1,
PROCESSING: 2, PROCESSING: 2,
DONE: 3 DONE: 3
}; };
$scope.state.ui = states.IDLE; // initial state $scope.state.ui = states.IDLE; // initial state
// //
@ -26,15 +30,15 @@ var LoginInitialCtrl = function($scope, $location, $routeParams) {
* Continue to key import screen * Continue to key import screen
*/ */
$scope.importKey = function() { $scope.importKey = function() {
if (!$scope.state.agree) { if (!$scope.agree) {
$scope.onError({ displayError(new Error(termsMsg));
message: termsMsg
});
return; return;
} }
$scope.errMsg = undefined;
// sing up to newsletter // sing up to newsletter
$scope.signUpToNewsletter(); newsletter.signup(emailAddress, $scope.newsletter);
// go to key import // go to key import
$location.path('/login-new-device'); $location.path('/login-new-device');
}; };
@ -43,77 +47,47 @@ var LoginInitialCtrl = function($scope, $location, $routeParams) {
* Continue to keygen * Continue to keygen
*/ */
$scope.generateKey = function() { $scope.generateKey = function() {
if (!$scope.state.agree) { if (!$scope.agree) {
$scope.onError({ displayError(new Error(termsMsg));
message: termsMsg
});
return; return;
} }
$scope.errMsg = undefined;
// sing up to newsletter // sing up to newsletter
$scope.signUpToNewsletter(); newsletter.signup(emailAddress, $scope.newsletter);
// go to set keygen screen // go to set keygen screen
$scope.setState(states.PROCESSING); $scope.setState(states.PROCESSING);
setTimeout(function() {
emailDao.unlock({ emailDao.unlock({
passphrase: undefined // generate key without passphrase passphrase: undefined // generate key without passphrase
}, function(err) { }, function(err) {
if (err) { if (err) {
$scope.setState(states.IDLE); displayError(err);
$scope.onError(err);
return; return;
} }
appController._auth.storeCredentials(function(err) { appController._auth.storeCredentials(function(err) {
if (err) { if (err) {
return $scope.onError(err); displayError(err);
return;
} }
$location.path('/desktop'); $location.path('/desktop');
$scope.$apply(); $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.setState = function(state) {
$scope.state.ui = state; $scope.state.ui = state;
}; };
function displayError(err) {
$scope.setState(states.IDLE);
$scope.errMsg = err.errMsg || err.message;
$scope.$apply();
}
}; };
module.exports = LoginInitialCtrl; module.exports = LoginInitialCtrl;

View File

@ -14,7 +14,15 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) {
$scope.incorrect = false; $scope.incorrect = false;
$scope.confirmPassphrase = function() { $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; $scope.incorrect = false;
unlockCrypto(); unlockCrypto();
}; };
@ -23,7 +31,7 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) {
// check if user already has a public key on the key server // check if user already has a public key on the key server
emailDao._keychain.getUserKeyPair(userId, function(err, keypair) { emailDao._keychain.getUserKeyPair(userId, function(err, keypair) {
if (err) { if (err) {
$scope.onError(err); $scope.displayError(err);
return; return;
} }
@ -34,7 +42,7 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) {
try { try {
$scope.key.publicKeyArmored = pgp.extractPublicKey($scope.key.privateKeyArmored); $scope.key.publicKeyArmored = pgp.extractPublicKey($scope.key.privateKeyArmored);
} catch (e) { } catch (e) {
$scope.onError(new Error('Error parsing public key from private key!')); $scope.displayError(new Error('Error reading PGP key!'));
return; return;
} }
} }
@ -45,7 +53,7 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) {
privKeyParams = pgp.getKeyParams($scope.key.privateKeyArmored); privKeyParams = pgp.getKeyParams($scope.key.privateKeyArmored);
pubKeyParams = pgp.getKeyParams($scope.key.publicKeyArmored); pubKeyParams = pgp.getKeyParams($scope.key.publicKeyArmored);
} catch (e) { } catch (e) {
$scope.onError(new Error('Error reading key params!')); $scope.displayError(new Error('Error reading key params!'));
return; return;
} }
@ -74,7 +82,7 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) {
}, function(err) { }, function(err) {
if (err) { if (err) {
$scope.incorrect = true; $scope.incorrect = true;
$scope.onError(err); $scope.displayError(err);
return; return;
} }
@ -85,19 +93,26 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) {
function onUnlock(err) { function onUnlock(err) {
if (err) { if (err) {
$scope.onError(err); $scope.displayError(err);
return; return;
} }
appController._auth.storeCredentials(function(err) { appController._auth.storeCredentials(function(err) {
if (err) { if (err) {
return $scope.onError(err); $scope.displayError(err);
return;
} }
$location.path('/desktop'); $location.path('/desktop');
$scope.$apply(); $scope.$apply();
}); });
} }
$scope.displayError = function(err) {
$scope.busy = false;
$scope.errMsg = err.errMsg || err.message;
$scope.$apply();
};
}; };
var ngModule = angular.module('login-new-device', []); var ngModule = angular.module('login-new-device', []);
@ -117,7 +132,7 @@ ngModule.directive('fileReader', function() {
keyParts; keyParts;
if (index === -1) { if (index === -1) {
scope.onError(new Error('Error parsing private PGP key block!')); scope.displayError(new Error('Error parsing private PGP key block!'));
return; return;
} }

View File

@ -8,12 +8,116 @@ var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams) {
return; return;
} }
if (appController._emailDao) {
var keychain = appController._keychain, var keychain = appController._keychain,
emailDao = appController._emailDao, emailDao = appController._emailDao,
userId = emailDao._account.emailAddress; userId = emailDao._account.emailAddress;
}
$scope.step = 1; $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) { $scope.handlePaste = function(event) {
var evt = event; var evt = event;
if (evt.originalEvent) { if (evt.originalEvent) {
@ -34,99 +138,21 @@ var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams) {
$scope.code5 = value.slice(20, 24); $scope.code5 = value.slice(20, 24);
}; };
$scope.verifyRecoveryToken = function(callback) { //
if (!$scope.recoveryToken) { // helper functions
$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;
}
};
$scope.goTo = function(location) { $scope.goTo = function(location) {
$location.path(location); $location.path(location);
$scope.$apply(); $scope.$apply();
}; };
function displayError(err) {
$scope.busy = false;
$scope.incorrect = true;
$scope.errMsg = err.errMsg || err.message;
$scope.$apply();
}
}; };
module.exports = LoginPrivateKeyDownloadCtrl; module.exports = LoginPrivateKeyDownloadCtrl;

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
var ngModule = angular.module('woServices', []); var ngModule = angular.module('woServices');
ngModule.service('mailConfig', MailConfig); ngModule.service('mailConfig', MailConfig);
var cfg = require('../app-config').config; 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 { .page {
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
// allow scrolling on iOS
-webkit-overflow-scrolling: touch;
// disable text selection // disable text selection
user-select: none; user-select: none;

View File

@ -6,17 +6,19 @@
<main class="page__main"> <main class="page__main">
<h2 class="typo-title">Unlock mailbox</h2> <h2 class="typo-title">Unlock mailbox</h2>
<p class="typo-paragraph">Please enter your passphrase to unlock the mailbox.</p> <p class="typo-paragraph">Please enter your passphrase to unlock the mailbox.</p>
<form class="form"> <form class="form" name="form">
<!-- TODO add error messages --> <p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<!--<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"> <div class="form__row">
<input class="input-text input-text--big" type="password" ng-model="passphrase" ng-change="change()" <input type="password" ng-model="passphrase"
ng-class="{'input-text--error':incorrect}" placeholder="Passphrase" tabindex="1" focus-me="true"> 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>
<div class="form__row"> <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> <svg role="presentation"><use xlink:href="#icon-decrypted" /></svg>
Unlock Unlock
</button> </button>

View File

@ -12,13 +12,11 @@
</p> </p>
<form class="form" name="form"> <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="errMsg">{{errMsg}}</p>
<p class="form__error-message" ng-show="form.$invalid">Please fill out all required fields!</p>-->
<div class="form__row"> <div class="form__row">
<label class="input-checkbox"> <label class="input-checkbox">
<span class="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 class="input-checkbox__box"><svg role="presentation"><use xlink:href="#icon-check" /></svg></span>
</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> 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"> <div class="form__row">
<label class="input-checkbox"> <label class="input-checkbox">
<span class="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 class="input-checkbox__box"><svg role="presentation"><use xlink:href="#icon-check" /></svg></span>
</span> </span>
Stay up to date on Whiteout Networks products and important announcements. Stay up to date on Whiteout Networks products and important announcements.
</label> </label>
</div> </div>
<div class="form__row"> <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>
<div class="form__row"> <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> </div>
</form> </form>
</div> </div>
@ -47,7 +45,7 @@
<p class="typo-paragraph"> <p class="typo-paragraph">
Please stand by. This can take a while… Please stand by. This can take a while…
</p> </p>
<div class="u-text-center"> <div class="spinner-block spinner-block--standalone">
<span class="spinner spinner--big"></span> <span class="spinner spinner--big"></span>
</div> </div>
</div> </div>

View File

@ -17,23 +17,19 @@
</p> </p>
</fieldset> </fieldset>
<!-- TODO --> <p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<!--<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"> <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>
<div class="form__row"> <div class="form__row">
<input class="input-text" type="password" ng-model="passphrase" <input class="input-text" type="password" ng-model="passphrase"
ng-class="{'input-text--error':incorrect}" placeholder="Passphrase" tabindex="2"> ng-class="{'input-text--error':incorrect}" placeholder="Passphrase" tabindex="2">
</div> </div>
<!-- TODO --> <div class="spinner-block" ng-show="busy">
<!--<div class="spinner-block" ng-show="busy">
<span class="spinner spinner--big"></span> <span class="spinner spinner--big"></span>
</div>--> </div>
<div class="form__row"> <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> </div>
</form> </form>
<p class="typo-paragraph"> <p class="typo-paragraph">

View File

@ -8,16 +8,18 @@
<div ng-show="step === 1"> <div ng-show="step === 1">
<h2 class="typo-title">Key sync</h2> <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> <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"> <form class="form" name="tokenForm">
<!-- TODO add error messages --> <p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<!--<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"> <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>
<div class="form__row"> <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> </div>
</form> </form>
@ -37,23 +39,24 @@
<div ng-show="step === 2"> <div ng-show="step === 2">
<h2 class="typo-title">Key sync</h2> <h2 class="typo-title">Key sync</h2>
<p class="typo-paragraph">Please enter the keychain code you wrote down during sync setup.</p> <p class="typo-paragraph">Please enter the keychain code you wrote down during sync setup.</p>
<form class="form"> <form class="form" name="codeForm">
<!-- TODO add error messages --> <p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<!--<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"> <div class="form__row">
<div class="input-code"> <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="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> - <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> - <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> - <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> - <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"> <input type="text" class="input-text" size="4" maxlength="4" ng-model="code5" required>
</div> </div>
</div> </div>
<div class="spinner-block" ng-show="busy">
<span class="spinner spinner--big"></span>
</div>
<div class="form__row"> <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> </div>
</form> </form>
<p class="typo-paragraph"> <p class="typo-paragraph">

View File

@ -108,7 +108,7 @@
<span class="spinner spinner--big"></span> <span class="spinner spinner--big"></span>
</div> </div>
<div class="form__row"> <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> </div>
</form> </form>
</main> </main>

View File

@ -12,10 +12,6 @@
</p> </p>
<form class="form" name="form"> <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"> <div class="form__row">
<input class="input-text" type="password" ng-model="oldPassphrase" placeholder="Current passphrase" tabindex="1" focus-me="true"> <input class="input-text" type="password" ng-model="oldPassphrase" placeholder="Current passphrase" tabindex="1" focus-me="true">
</div> </div>

View File

@ -35,6 +35,7 @@ describe('Login (existing user) Controller unit test', function() {
location = $location; location = $location;
scope = $rootScope.$new(); scope = $rootScope.$new();
scope.state = {}; scope.state = {};
scope.form = {};
ctrl = $controller(LoginExistingCtrl, { ctrl = $controller(LoginExistingCtrl, {
$scope: scope, $scope: scope,
$routeParams: {} $routeParams: {}
@ -50,20 +51,7 @@ describe('Login (existing user) Controller unit test', function() {
describe('initial state', function() { describe('initial state', function() {
it('should be well defined', function() { it('should be well defined', function() {
expect(scope.buttonEnabled).to.be.true; expect(scope.incorrect).to.be.undefined;
expect(scope.incorrect).to.be.false;
expect(scope.change).to.exist;
expect(scope.confirmPassphrase).to.exist;
});
});
describe('functionality', function() {
describe('change', function() {
it('should set incorrect to false', function() {
scope.incorrect = true;
scope.change();
expect(scope.incorrect).to.be.false;
}); });
}); });
@ -79,7 +67,6 @@ describe('Login (existing user) Controller unit test', function() {
}).yields(); }).yields();
authMock.storeCredentials.yields(); authMock.storeCredentials.yields();
scope.confirmPassphrase(); scope.confirmPassphrase();
expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; expect(keychainMock.getUserKeyPair.calledOnce).to.be.true;
@ -88,26 +75,13 @@ describe('Login (existing user) Controller unit test', function() {
expect(pathSpy.calledWith('/desktop')).to.be.true; expect(pathSpy.calledWith('/desktop')).to.be.true;
}); });
it('should not do anything without passphrase', function() { it('should not work when keypair unavailable', 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; scope.passphrase = passphrase;
keychainMock.getUserKeyPair.withArgs(emailAddress).yields(new Error('asd')); 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() { describe('Login (initial user) Controller unit test', function() {
var scope, ctrl, location, origEmailDao, emailDaoMock, var scope, ctrl, location, origEmailDao, emailDaoMock,
origAuth, authMock, origAuth, authMock, newsletterStub,
emailAddress = 'fred@foo.com', emailAddress = 'fred@foo.com',
keyId, expectedKeyId, keyId, expectedKeyId,
cryptoMock; cryptoMock;
@ -31,17 +31,19 @@ describe('Login (initial user) Controller unit test', function() {
emailAddress: emailAddress, emailAddress: emailAddress,
}; };
angular.module('logininitialtest', []); angular.module('logininitialtest', ['woServices']);
mocks.module('logininitialtest'); mocks.module('logininitialtest');
mocks.inject(function($rootScope, $controller, $location) { mocks.inject(function($rootScope, $controller, $location, newsletter) {
scope = $rootScope.$new(); scope = $rootScope.$new();
location = $location; location = $location;
newsletterStub = sinon.stub(newsletter, 'signup');
scope.state = { scope.state = {
ui: {} ui: {}
}; };
ctrl = $controller(LoginInitialCtrl, { ctrl = $controller(LoginInitialCtrl, {
$scope: scope, $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() { describe('go to import key', function() {
var signUpToNewsletterStub; it('should not continue if terms are not accepted', function() {
beforeEach(function() { scope.agree = undefined;
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();
};
scope.importKey(); scope.importKey();
expect(scope.errMsg).to.contain('Terms');
expect(newsletterStub.called).to.be.false;
}); });
it('should work', function() { it('should work', function() {
scope.state.agree = true; scope.agree = true;
scope.importKey(); scope.importKey();
expect(signUpToNewsletterStub.calledOnce).to.be.true; expect(newsletterStub.calledOnce).to.be.true;
expect(location.$$path).to.equal('/login-new-device'); expect(location.$$path).to.equal('/login-new-device');
}); });
}); });
describe('generate key', function() { describe('generate key', function() {
var signUpToNewsletterStub; it('should not continue if terms are not accepted', function() {
beforeEach(function() { scope.agree = undefined;
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();
};
scope.generateKey(); 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) { it('should fail due to error in emailDao.unlock', function() {
scope.state.agree = true; scope.agree = true;
emailDaoMock.unlock.withArgs({ emailDaoMock.unlock.withArgs({
passphrase: undefined passphrase: undefined
}).yields(new Error()); }).yields(new Error('asdf'));
authMock.storeCredentials.yields(); 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(); 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) { it('should unlock crypto', function() {
scope.state.agree = true; scope.agree = true;
emailDaoMock.unlock.withArgs({ emailDaoMock.unlock.withArgs({
passphrase: undefined passphrase: undefined
}).yields(); }).yields();
authMock.storeCredentials.yields(); authMock.storeCredentials.yields();
scope.$apply = function() { scope.generateKey();
expect(scope.errMsg).to.not.exist;
expect(scope.state.ui).to.equal(2); expect(scope.state.ui).to.equal(2);
expect(newsletterStub.called).to.be.true;
expect(location.$$path).to.equal('/desktop'); expect(location.$$path).to.equal('/desktop');
expect(emailDaoMock.unlock.calledOnce).to.be.true; expect(emailDaoMock.unlock.calledOnce).to.be.true;
done();
};
scope.generateKey();
}); });
}); });
}); });

View File

@ -37,6 +37,7 @@ describe('Login (new device) Controller unit test', function() {
scope.state = { scope.state = {
ui: {} ui: {}
}; };
scope.form = {};
ctrl = $controller(LoginNewDeviceCtrl, { ctrl = $controller(LoginNewDeviceCtrl, {
$scope: scope, $scope: scope,
$routeParams: {} $routeParams: {}
@ -103,7 +104,7 @@ describe('Login (new device) Controller unit test', function() {
expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; 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.passphrase = passphrase;
scope.key = { scope.key = {
privateKeyArmored: 'b' privateKeyArmored: 'b'
@ -123,19 +124,15 @@ describe('Login (new device) Controller unit test', function() {
errMsg: 'yo mamma.' errMsg: 'yo mamma.'
}); });
scope.onError = function(err) {
expect(err.errMsg).to.equal('yo mamma.');
done();
};
scope.confirmPassphrase(); scope.confirmPassphrase();
expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; expect(keychainMock.getUserKeyPair.calledOnce).to.be.true;
expect(emailDaoMock.unlock.calledOnce).to.be.true; expect(emailDaoMock.unlock.calledOnce).to.be.true;
expect(keychainMock.putUserKeyPair.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.passphrase = passphrase;
scope.key = { scope.key = {
privateKeyArmored: 'b' privateKeyArmored: 'b'
@ -154,33 +151,28 @@ describe('Login (new device) Controller unit test', function() {
errMsg: 'yo mamma.' errMsg: 'yo mamma.'
}); });
scope.onError = function(err) {
expect(err.errMsg).to.equal('yo mamma.');
done();
};
scope.confirmPassphrase(); scope.confirmPassphrase();
expect(scope.incorrect).to.be.true; expect(scope.incorrect).to.be.true;
expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; expect(keychainMock.getUserKeyPair.calledOnce).to.be.true;
expect(emailDaoMock.unlock.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.passphrase = passphrase;
scope.key = {
privateKeyArmored: 'b'
};
keychainMock.getUserKeyPair.withArgs(emailAddress).yields({ keychainMock.getUserKeyPair.withArgs(emailAddress).yields({
errMsg: 'yo mamma.' errMsg: 'yo mamma.'
}); });
scope.onError = function(err) {
expect(err.errMsg).to.equal('yo mamma.');
done();
};
scope.confirmPassphrase(); scope.confirmPassphrase();
expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; 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) { mocks.inject(function($controller, $rootScope) {
scope = $rootScope.$new(); scope = $rootScope.$new();
scope.state = {}; scope.state = {};
scope.tokenForm = {};
scope.codeForm = {};
ctrl = $controller(LoginPrivateKeyDownloadCtrl, { ctrl = $controller(LoginPrivateKeyDownloadCtrl, {
$location: location, $location: location,
$scope: scope, $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() { describe('verifyRecoveryToken', function() {
var testKeypair = { var testKeypair = {
publicKey: { publicKey: {
@ -62,53 +93,35 @@ describe('Login Private Key Download Controller unit test', function() {
} }
}; };
it('should fail for empty recovery token', function(done) { it('should fail in keychain.getUserKeyPair', function() {
scope.onError = function(err) { keychainMock.getUserKeyPair.yields(new Error('asdf'));
expect(err).to.exist;
done();
};
scope.recoveryToken = undefined;
scope.verifyRecoveryToken(); scope.verifyRecoveryToken();
});
it('should fail in keychain.getUserKeyPair', function(done) { expect(scope.errMsg).to.exist;
keychainMock.getUserKeyPair.yields(42);
scope.onError = function(err) {
expect(err).to.exist;
expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; 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.getUserKeyPair.yields(null, testKeypair);
keychainMock.downloadPrivateKey.yields(42); keychainMock.downloadPrivateKey.yields(new Error('asdf'));
scope.recoveryToken = 'token';
scope.onError = function(err) { scope.verifyRecoveryToken();
expect(err).to.exist;
expect(scope.errMsg).to.exist;
expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; expect(keychainMock.getUserKeyPair.calledOnce).to.be.true;
expect(keychainMock.downloadPrivateKey.calledOnce).to.be.true; expect(keychainMock.downloadPrivateKey.calledOnce).to.be.true;
done();
};
scope.recoveryToken = 'token';
scope.verifyRecoveryToken();
}); });
it('should work', function(done) { it('should work', function() {
keychainMock.getUserKeyPair.yields(null, testKeypair); keychainMock.getUserKeyPair.yields(null, testKeypair);
keychainMock.downloadPrivateKey.yields(null, 'encryptedPrivateKey'); keychainMock.downloadPrivateKey.yields(null, 'encryptedPrivateKey');
scope.recoveryToken = 'token'; scope.recoveryToken = 'token';
scope.verifyRecoveryToken(function() {
scope.verifyRecoveryToken(function() {});
expect(scope.encryptedPrivateKey).to.equal('encryptedPrivateKey'); expect(scope.encryptedPrivateKey).to.equal('encryptedPrivateKey');
done();
});
}); });
}); });
@ -151,39 +164,20 @@ describe('Login Private Key Download Controller unit test', function() {
}; };
}); });
it('should fail on empty code', function(done) { it('should fail on decryptAndStorePrivateKeyLocally', function() {
scope.code0 = ''; keychainMock.decryptAndStorePrivateKeyLocally.yields(new Error('asdf'));
scope.code1 = '';
scope.code2 = '';
scope.code3 = '';
scope.code4 = '';
scope.code5 = '';
scope.onError = function(err) {
expect(err).to.exist;
done();
};
scope.decryptAndStorePrivateKeyLocally(); scope.decryptAndStorePrivateKeyLocally();
});
it('should fail on decryptAndStorePrivateKeyLocally', function(done) { expect(scope.errMsg).to.exist;
keychainMock.decryptAndStorePrivateKeyLocally.yields(42);
scope.onError = function(err) {
expect(err).to.exist;
expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true; expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true;
done();
};
scope.decryptAndStorePrivateKeyLocally();
}); });
it('should goto /login-existing on emailDao.unlock fail', function(done) { it('should goto /login-existing on emailDao.unlock fail', function(done) {
keychainMock.decryptAndStorePrivateKeyLocally.yields(null, { keychainMock.decryptAndStorePrivateKeyLocally.yields(null, {
encryptedKey: 'keyArmored' encryptedKey: 'keyArmored'
}); });
emailDaoMock.unlock.yields(42); emailDaoMock.unlock.yields(new Error('asdf'));
scope.goTo = function(location) { scope.goTo = function(location) {
expect(location).to.equal('/login-existing'); 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() { describe('goTo', function() {
it('should work', function(done) { it('should work', function(done) {
mocks.inject(function($controller, $rootScope, $location) { 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);
}));
});
});