diff --git a/browserid/static/dialog/qunit.html b/browserid/static/dialog/qunit.html index 108cc60fd8fbc23e0105384c865a527e58e19b92..a4baede7d5e5bf3d447ce11f8d51c7901a372a71 100644 --- a/browserid/static/dialog/qunit.html +++ b/browserid/static/dialog/qunit.html @@ -27,6 +27,11 @@ </div> </div> + <span id="email"></span> + <span id="cannotconfirm" class="error">Cannot confirm</span> + <span id="cannotcommunicate" class="error">Cannot communicate</span> + <span id="siteinfo" class="error"><span id="origin"></span></span> + <span class=".hint">Hint</span> </div> <ol id="qunit-tests"></ol> <div id="qunit-test-area"></div> diff --git a/browserid/static/dialog/resources/network.js b/browserid/static/dialog/resources/network.js index 0e587f1ce306df486eaf0625ed03a91762895eb2..b447aa2f08353db38d5f928a4f9b66154e25591b 100644 --- a/browserid/static/dialog/resources/network.js +++ b/browserid/static/dialog/resources/network.js @@ -293,6 +293,27 @@ BrowserID.Network = (function() { }); }, + /** + * Call with a token to prove an email address ownership. + * @method completeEmailRegistration + * @param {string} token - token proving email ownership. + * @param {function} [onSuccess] - Callback to call when complete. Called + * with one boolean parameter that specifies the validity of the token. + * @param {function} [onFailure] - Called on XHR failure. + */ + completeEmailRegistration: function(token, onSuccess, onFailure) { + post({ + url: "/wsapi/complete_email_addition", + data: { + token: token + }, + success: function(status, textStatus, jqXHR) { + if (onSuccess) onSuccess(status.success); + }, + error: onFailure + }); + }, + /** * Request a password reset for the given email address. * @method requestPasswordReset @@ -337,26 +358,6 @@ BrowserID.Network = (function() { } }, - /** - * Call with a token to prove an email address ownership. - * @method completeEmailRegistration - * @param {string} token - token proving email ownership. - * @param {function} [onSuccess] - Callback to call when complete. Called - * with one boolean parameter that specifies the validity of the token. - * @param {function} [onFailure] - Called on XHR failure. - */ - completeEmailRegistration: function(token, onSuccess, onFailure) { - post({ - url: "/wsapi/complete_email_addition", - data: { - token: token - }, - success: function(status, textStatus, jqXHR) { - if (onSuccess) onSuccess(status.success); - }, - error: onFailure - }); - }, /** * Cancel the current user"s account. diff --git a/browserid/static/dialog/resources/user.js b/browserid/static/dialog/resources/user.js index c5117db0399743dc12d69e6c41e21279e0f43f6d..f3740d643b6f8119e3fcd00a23cd1a752043ea29 100644 --- a/browserid/static/dialog/resources/user.js +++ b/browserid/static/dialog/resources/user.js @@ -277,7 +277,7 @@ BrowserID.User = (function() { * identity. * @method cancelUser * @param {function} [onSuccess] - Called whenever complete. - * @param {function} [onFailure] - called on failure. + * @param {function} [onFailure] - called on error. */ cancelUser: function(onSuccess, onFailure) { network.cancelUser(function() { @@ -293,7 +293,7 @@ BrowserID.User = (function() { * Log the current user out. * @method logoutUser * @param {function} [onSuccess] - Called whenever complete. - * @param {function} [onFailure] - called on failure. + * @param {function} [onFailure] - called on error. */ logoutUser: function(onSuccess, onFailure) { network.logout(function() { @@ -309,7 +309,7 @@ BrowserID.User = (function() { * be called. * @method syncEmails * @param {function} [onSuccess] - Called whenever complete. - * @param {function} [onFailure] - Called on failure. + * @param {function} [onFailure] - Called on error. */ syncEmails: function(onSuccess, onFailure) { cleanupIdentities(); @@ -365,7 +365,7 @@ BrowserID.User = (function() { * @param {function} [onSuccess] - Called when check is complete with one * boolean parameter, authenticated. authenticated will be true if user is * authenticated, false otw. - * @param {function} [onFailure] - Called on failure. + * @param {function} [onFailure] - Called on error. */ checkAuthentication: function(onSuccess, onFailure) { network.checkAuth(function(authenticated) { @@ -384,7 +384,7 @@ BrowserID.User = (function() { * but before sync starts. Useful for displaying status messages about the * sync taking a moment. * @param {function} [onComplete] - Called on sync completion. - * @param {function} [onFailure] - Called on failure. + * @param {function} [onFailure] - Called on error. */ checkAuthenticationAndSync: function(onSuccess, onComplete, onFailure) { var self=this; @@ -413,7 +413,7 @@ BrowserID.User = (function() { * @param {string} email - Email address to authenticate. * @param {string} password - Password. * @param {function} [onComplete] - Called on sync completion. - * @param {function} [onFailure] - Called on failure. + * @param {function} [onFailure] - Called on error. */ authenticate: function(email, password, onComplete, onFailure) { var self=this; @@ -452,6 +452,7 @@ BrowserID.User = (function() { var self = this; network.addEmail(email, origin, function(added) { if (added) { + localStorage.initiatingOrigin = self.getHostname(); // we no longer send the keypair, since we will certify it later. if (onSuccess) { onSuccess(added); @@ -471,12 +472,42 @@ BrowserID.User = (function() { registrationPoll(network.checkEmailRegistration, email, onSuccess, onFailure); }, + /** + * Verify a users email address given by the token + * @method verifyEmail + * @param {string} token + * @param {function} [onSuccess] - Called on success. + * Called with an object with valid, email, and origin if valid, called + * with only valid otw. + * @param {function} [onFailure] - Called on error. + */ + verifyEmail: function(token, onSuccess, onFailure) { + network.emailForVerificationToken(token, function (email) { + var invalidInfo = { valid: false }; + if (email) { + network.completeEmailRegistration(token, function (valid) { + var info = valid ? { + valid: valid, + email: email, + origin: localStorage.initiatingOrigin + } : invalidInfo; + + localStorage.removeItem("initiatingOrigin"); + + if (onSuccess) onSuccess(info); + }, onFailure); + } else if(onSuccess) { + onSuccess(invalidInfo); + } + }, onFailure); + }, + /** * Remove an email address. * @method removeEmail * @param {string} email - Email address to remove. * @param {function} [onSuccess] - Called when complete. - * @param {function} [onFailure] - Called on failure. + * @param {function} [onFailure] - Called on error. */ removeEmail: function(email, onSuccess, onFailure) { if(storage.getEmail(email)) { @@ -513,7 +544,7 @@ BrowserID.User = (function() { * @method getAssertion * @param {string} email - Email to get assertion for. * @param {function} [onSuccess] - Called with assertion on success. - * @param {function} [onFailure] - Called on failure. + * @param {function} [onFailure] - Called on error. */ getAssertion: function(email, onSuccess, onFailure) { // we use the current time from the browserid servers diff --git a/browserid/static/dialog/test/qunit/pages/add_email_address_test.js b/browserid/static/dialog/test/qunit/pages/add_email_address_test.js new file mode 100644 index 0000000000000000000000000000000000000000..bbeb897bd53a3e35c8d774232ff26aee3480cfdf --- /dev/null +++ b/browserid/static/dialog/test/qunit/pages/add_email_address_test.js @@ -0,0 +1,111 @@ +/*jshint browsers:true, forin: true, laxbreak: true */ +/*global steal: true, test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla BrowserID. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +steal.plugins("jquery").then("/js/pages/add_email_address", function() { + "use strict"; + + var bid = BrowserID, + network = bid.Network, + emailForVerificationTokenFailure = false, + completeEmailRegistrationFailure = false, + validToken = true; + + var netMock = { + emailForVerificationToken: function(token, onSuccess, onFailure) { + emailForVerificationTokenFailure ? onFailure() : onSuccess("testuser@testuser.com"); + }, + + completeEmailRegistration: function(token, onSuccess, onFailure) { + completeEmailRegistrationFailure ? onFailure() : onSuccess(validToken); + } + }; + + module("pages/add_email_address", { + setup: function() { + BrowserID.User.setNetwork(netMock); + emailForVerificationTokenFailure = completeEmailRegistrationFailure = false; + validToken = true; + $(".error").stop().hide(); + $("#origin").text(""); + }, + teardown: function() { + BrowserID.User.setNetwork(network); + $(".error").stop().hide(); + $("#origin").text(""); + } + }); + + test("addEmailAddress with good token and site", function() { + localStorage.initiatingOrigin = "browserid.org"; + + bid.addEmailAddress("token"); + + equal($("#email").text(), "testuser@testuser.com", "email set"); + ok($("#siteinfo").is(":visible"), "siteinfo is visible when we say what it is"); + equal($("#origin").text(), "browserid.org", "origin is updated"); + }); + + test("addEmailAddress with good token and nosite", function() { + bid.addEmailAddress("token"); + + equal($("#email").text(), "testuser@testuser.com", "email set"); + equal($("#siteinfo").is(":visible"), false, "siteinfo is not visible without having it"); + equal($("#origin").text(), "", "origin is not updated"); + }); + + test("addEmailAddress with bad token", function() { + validToken = false; + + bid.addEmailAddress("token"); + ok($("#cannotconfirm").is(":visible"), "cannot confirm box is visible"); + }); + + test("addEmailAddress with emailForVerficationToken XHR failure", function() { + validToken = true; + emailForVerificationTokenFailure = true; + bid.addEmailAddress("token"); + + ok($("#cannotcommunicate").is(":visible"), "cannot communicate box is visible"); + }); + + test("addEmailAddress with completeEmailRegistration XHR failure", function() { + validToken = true; + completeEmailRegistrationFailure = true; + bid.addEmailAddress("token"); + + ok($("#cannotcommunicate").is(":visible"), "cannot communicate box is visible"); + }); +}); diff --git a/browserid/static/dialog/test/qunit/qunit.js b/browserid/static/dialog/test/qunit/qunit.js index 435935bdb970b11177eb07a37efc7aaa60a63e99..1733ac5548a0e89a31aa0f86527d287d7fc4d2ca 100644 --- a/browserid/static/dialog/test/qunit/qunit.js +++ b/browserid/static/dialog/test/qunit/qunit.js @@ -13,6 +13,7 @@ steal("/dialog/resources/browserid.js", .views('testBodyTemplate.ejs') .views('wait.ejs') .then("browserid_unit_test") + .then("pages/add_email_address_test") .then("controllers/page_controller_unit_test") .then("resources/validation_unit_test") .then("resources/storage_unit_test") diff --git a/browserid/static/dialog/test/qunit/resources/user_unit_test.js b/browserid/static/dialog/test/qunit/resources/user_unit_test.js index 6f980c1d627340c6b153f900221dcf698bf0a27f..4ee2445e4b70762eb2ba6bb94d2c42fdb03c082c 100644 --- a/browserid/static/dialog/test/qunit/resources/user_unit_test.js +++ b/browserid/static/dialog/test/qunit/resources/user_unit_test.js @@ -56,7 +56,8 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio userCheckCount = 0, emailCheckCount = 0, registrationResponse, - xhrFailure = false; + xhrFailure = false, + validToken = true; var netStub = { reset: function() { @@ -98,6 +99,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio xhrFailure ? onFailure() : onSuccess(status); }, + emailForVerificationToken: function(token, onSuccess, onFailure) { + xhrFailure ? onFailure() : onSuccess("testuser@testuser.com"); + }, + + completeEmailRegistration: function(token, onSuccess, onFailure) { + xhrFailure ? onFailure() : onSuccess(validToken); + }, + removeEmail: function(email, onSuccess, onFailure) { xhrFailure ? onFailure() : onSuccess(); }, @@ -200,6 +209,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio netStub.reset(); userCheckCount = 0; emailCheckCount = 0; + validToken = true; }, teardown: function() { lib.setNetwork(BrowserID.Network); @@ -528,6 +538,9 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio var identities = lib.getStoredEmailKeypairs(); equal(false, "testemail@testemail.com" in identities, "Our new email is not added until confirmation."); + + equal(localStorage.initiatingOrigin, lib.getHostname(), "initiatingOrigin is stored"); + start(); }, failure("addEmail failure")); @@ -607,6 +620,49 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio }); + test("verifyEmail with a good token", function() { + localStorage.initiatingOrigin = "browserid.org"; + lib.verifyEmail("token", function onSuccess(info) { + + ok(info.valid, "token was valid"); + equal(info.email, "testuser@testuser.com", "email part of info"); + equal(info.origin, "browserid.org", "origin in info"); + equal(localStorage.initiatingOrigin, null, "initiating origin was removed"); + + start(); + }, failure("verifyEmail failure")); + + stop(); + }); + + test("verifyEmail with a bad token", function() { + validToken = false; + + lib.verifyEmail("token", function onSuccess(info) { + + equal(info.valid, false, "bad token calls onSuccess with a false validity"); + + start(); + }, failure("verifyEmail failure")); + + stop(); + + }); + + test("verifyEmail with an XHR failure", function() { + xhrFailure = true; + + lib.verifyEmail("token", function onSuccess(info) { + ok(false, "xhr failure should never succeed"); + start(); + }, function() { + ok(true, "xhr failure should always be a failure"); + start(); + }); + + stop(); + }); + test("syncEmailKeypair with successful sync", function() { syncValid = true; lib.syncEmailKeypair("testemail@testemail.com", function(keypair) { diff --git a/browserid/static/js/pages/add_email_address.js b/browserid/static/js/pages/add_email_address.js index e5c91d305cadf9e79c50636fd36154abce90db4b..b1ab0fde7a048cef6f326ac9dd9e0c5c50fc80fe 100644 --- a/browserid/static/js/pages/add_email_address.js +++ b/browserid/static/js/pages/add_email_address.js @@ -36,28 +36,34 @@ (function() { "use strict"; - - function emailRegistrationSuccess() { + + var ANIMATION_TIME=250; + function emailRegistrationSuccess(info) { $(".hint").hide(); - $("#congrats").fadeIn(250, function() { - $("body").delay(1000).fadeOut(500, function() { - // if the close didn't work, then let's redirect the the main page where they'll - // get to see the ids that they've created. - document.location = '/'; - }); - }); + + $("#email").text(info.email); + + if (info.origin) { + $("#origin").text(info.origin); + $("#siteinfo").show(); + } + + $("#congrats").fadeIn(ANIMATION_TIME); } function showError(el) { $(".hint").hide(); - $(el).fadeIn(250); + $(el).fadeIn(ANIMATION_TIME); } BrowserID.addEmailAddress = function(token) { - BrowserID.Network.completeEmailRegistration(token, function onSuccess(valid) { - if (valid) { - emailRegistrationSuccess(); - } else { + var user = BrowserID.User; + + user.verifyEmail(token, function onSuccess(info) { + if (info.valid) { + emailRegistrationSuccess(info); + } + else { showError("#cannotconfirm"); } }, function onFailure() { diff --git a/browserid/views/verifyemail.ejs b/browserid/views/verifyemail.ejs index 766c84b76d2a307a6108f6c865e08411d381ded3..83d3ffbf5e2b94b7aceb5a64df6afa971dcd98ea 100644 --- a/browserid/views/verifyemail.ejs +++ b/browserid/views/verifyemail.ejs @@ -11,7 +11,14 @@ <div id="congrats" class="serif"> - Your email address has been verified! + <p> + <strong id="email">Your address</strong> has been verified! + + <span id="siteinfo"> + Your new address has been used to sign in to <strong id="origin"></strong>, + which will be in its original window or tab. + </span> + </p> </div> </div> </div>