diff --git a/lib/wsapi/update_password.js b/lib/wsapi/update_password.js index b7698696e35f9f4e39cc2640c2a59f22b3b1e7b4..9cbad100ff263c6a7d80f23cf823d20b41f5390d 100644 --- a/lib/wsapi/update_password.js +++ b/lib/wsapi/update_password.js @@ -23,6 +23,11 @@ exports.process = function(req, res) { return res.json({ success: false }); } + if (!success) { + logger.info("password update fails, incorrect old password"); + return res.json({ success: false }); + } + logger.info("updating password for email " + req.session.authenticatedUser); wsapi.bcryptPassword(req.body.newpass, function(err, hash) { if (err) { diff --git a/resources/static/css/style.css b/resources/static/css/style.css index 37e2a3a01a27c09b324110e2671851b781b67bd8..8b23ff1b3a6ffce815152264157a5276596c481c 100644 --- a/resources/static/css/style.css +++ b/resources/static/css/style.css @@ -271,38 +271,36 @@ div.steps { font-weight: normal; } -#manage .buttonrow { - margin: 72px 0 14px; +#manage section { + margin-bottom: 20px; +} + +.buttonrow { + margin: 0 0 14px; } -#manage .buttonrow button { +.buttonrow > h2 { + display: inline-block; + font-size: 1em; +} + +#manage button { line-height: 20px; height: 24px; - width: 48px; font-size: 12px; } -#manageAccounts { - background-color: #37A6FF; - border: 1px solid #37A6FF; - text-shadow: -1px -1px 0 #37A6FF; - cursor: pointer; - - -webkit-box-shadow: 0 0 0 1px #76C2FF inset; - -moz-box-shadow: 0 0 0 1px #76C2FF inset; - -o-box-shadow: 0 0 0 1px #76C2FF inset; - box-shadow: 0 0 0 1px #76C2FF inset; - - background-image: -moz-linear-gradient(#76C2FF 0pt, #37A6FF 100%); - background-image: -o-linear-gradient(#76C2FF 0pt, #37A6FF 100%); - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #76C2FF), color-stop(100%, #37A6FF)); +.buttonrow > button { + width: 48px; } -.edit #manageAccounts { +.buttonrow > .edit { } + +.edit .buttonrow > .edit { display: none; } -#cancelManage { +.buttonrow > .done { display: none; background-color: #006EC6; border: 1px solid #003E70; @@ -319,13 +317,12 @@ div.steps { background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #3AA7FF), color-stop(100%, #006EC6)); } -.edit #cancelManage { +.edit .buttonrow > .done { display: inline-block; } #manage #emailList { list-style-type: none; - margin: 0 0 72px 0; border-top: 1px solid #eee; } @@ -376,10 +373,7 @@ div.steps { button.delete { background-color: #EA7676; - height: 24px; - vertical-align: middle; border: 1px solid #B13D3D; - font-size: 12px; font-weight: bold; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #fff; @@ -417,6 +411,32 @@ button.delete:active { background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #C84343), color-stop(100%, #AA3D3D)); } +#edit_password { + margin-bottom: 10px; +} + +#edit_password label { + width: 40%; + display: inline-block; +} + +#edit_password input[type=password] { + width: 40%; +} + +.showedit { + opacity: 0; + -webkit-transition: all 500ms; + -moz-transition: all 500ms; + -ms-transition: all 500ms; + -o-transition: all 500ms; + transition: all 500ms; +} + +.edit .showedit { + opacity: 1; +} + #disclaimer { color: #888; text-align: right; @@ -683,7 +703,7 @@ a.forgot { } -header { +#wrapper > header { position: absolute; top: 0; font-weight: bold; @@ -725,7 +745,7 @@ header a.signIn:hover, header a.signOut:hover { display: inline; } -header, +#wrapper > header, footer { display: block; width: 100%; diff --git a/resources/static/dialog/controllers/actions.js b/resources/static/dialog/controllers/actions.js index 9e253e9ae89924c3918bbc39caa337acf1a9a83f..bd2725383de366afee6137f3be1b4b030c3ad932 100644 --- a/resources/static/dialog/controllers/actions.js +++ b/resources/static/dialog/controllers/actions.js @@ -1,4 +1,4 @@ -/*jshint brgwser:true, jQuery: true, forin: true, laxbreak:true */ +/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */ /*global _: true, BrowserID: true, PageController: true */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 @@ -168,8 +168,15 @@ BrowserID.Modules.Actions = (function() { authenticated: authenticated }); }, self.getErrorDialog(errors.checkAuthentication)); - } + }, + doVerifyPrimaryUser: function(info) { + startService("verify_primary_user", info); + }, + + doPrimaryUserVerified: function() { + // XXX we've gotta do something here too. + } }); sc = Module.sc; diff --git a/resources/static/dialog/controllers/authenticate.js b/resources/static/dialog/controllers/authenticate.js index 3149d5aab815d0a5ddc77a29ea7594bed203e44d..9d09e264f90511f68fe84469a8ec68efe9745585 100644 --- a/resources/static/dialog/controllers/authenticate.js +++ b/resources/static/dialog/controllers/authenticate.js @@ -128,7 +128,6 @@ BrowserID.Modules.Authenticate = (function() { } function createUserState() { - var self=this; self.publish("create_user"); diff --git a/resources/static/dialog/controllers/verify_primary_user.js b/resources/static/dialog/controllers/verify_primary_user.js new file mode 100644 index 0000000000000000000000000000000000000000..2725849454c3e8531eaf96869170d93157a42614 --- /dev/null +++ b/resources/static/dialog/controllers/verify_primary_user.js @@ -0,0 +1,60 @@ +/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */ +/*global _: true, BrowserID: true, PageController: 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 ***** */ +BrowserID.Modules.VerifyPrimaryUser = (function() { + "use strict"; + + var bid = BrowserID, + sc; + + var Module = bid.Modules.PageModule.extend({ + start: function(data) { + var self=this; + + data = data || {}; + + // XXX YEEHA! We've gotta set some variables and redirect off to the + // IdPs page. + sc.start.call(this, data); + } + + }); + + sc = Module.sc; + + return Module; +}()); + diff --git a/resources/static/dialog/resources/helpers.js b/resources/static/dialog/resources/helpers.js index 5c5d516672d8ad92e56d7f176732b8877466448c..4449bc51858fa427a4545521683c5e46efcf56b5 100644 --- a/resources/static/dialog/resources/helpers.js +++ b/resources/static/dialog/resources/helpers.js @@ -91,17 +91,50 @@ } function createUser(email, callback) { + function complete(status) { + callback && callback(status); + } + var self=this; - user.createUser(email, function(staged) { - if (staged) { - self.close("user_staged", { - email: email - }); - } - else { - tooltip.showTooltip("#could_not_add"); + user.createUser(email, function(status) { + switch(status) { + case "secondary.already_added": + // XXX how to handle this - createUser should not be called on + // already existing addresses, so this path should not be called. + tooltip.showTooltip("#already_registered"); + complete(false); + break; + case "secondary.verify": + self.close("user_staged", { + email: email + }); + complete(true); + break; + case "secondary.could_not_add": + tooltip.showTooltip("#could_not_add"); + complete(false); + break; + case "primary.already_added": + // XXX Is this status possible? + break; + case "primary.verified": + self.close("primary_user_verified", { + email: email + }); + complete(true); + break; + case "primary.verify": + self.close("primary_verify_user", { + email: email + }); + complete(true); + break; + case "primary.could_not_add": + // XXX Can this happen? + break; + default: + break; } - if (callback) callback(staged); }, self.getErrorDialog(errors.createUser, callback)); } diff --git a/resources/static/dialog/resources/state_machine.js b/resources/static/dialog/resources/state_machine.js index beb9f72d2cc2bad0a89ef92158b2544abc2c79c4..00f401da5f9927efbbaa3a4dc054b139fd4b9a6d 100644 --- a/resources/static/dialog/resources/state_machine.js +++ b/resources/static/dialog/resources/state_machine.js @@ -118,7 +118,6 @@ var authenticated = info.authenticated; if (self.requiredEmail) { - // XXX get this out of here and into the state machine! gotoState("doAuthenticateWithRequiredEmail", { email: self.requiredEmail, authenticated: authenticated @@ -147,6 +146,14 @@ gotoState("doEmailConfirmed"); }); + subscribe("primary_user_verified", function(msg, info) { + gotoState("doPrimaryUserVerified", info); + }); + + subscribe("primary_verify_user", function(msg, info) { + gotoState("doVerifyPrimaryUser", info); + }); + subscribe("authenticate_with_required_email", function(msg, info) { gotoState("doAuthenticateWithRequiredEmail", info); }); diff --git a/resources/static/dialog/start.js b/resources/static/dialog/start.js index a6787584ab511e2a3559278560f33ea12aae4733..944649ef074790d16f4c2c782cbfb2bd1720e670 100644 --- a/resources/static/dialog/start.js +++ b/resources/static/dialog/start.js @@ -18,6 +18,7 @@ moduleManager.register("forgot_password", modules.ForgotPassword); moduleManager.register("pick_email", modules.PickEmail); moduleManager.register("required_email", modules.RequiredEmail); + moduleManager.register("verify_primary_user", modules.VerifyPrimaryUser); moduleManager.start("dialog"); } diff --git a/resources/static/dialog/views/authenticate.ejs b/resources/static/dialog/views/authenticate.ejs index 8ac297c96a0d9ec2c157b7cb486537db3e1e03a1..376d2e6a1064890ac3a15bb8f3c0b8213dfa5959 100644 --- a/resources/static/dialog/views/authenticate.ejs +++ b/resources/static/dialog/views/authenticate.ejs @@ -17,6 +17,10 @@ <div id="could_not_add" class="tooltip" for="email"> We just sent an email to that address! If you really want to send another, wait a minute or two and try again. </div> + + <div id="already_registered" class="tooltip" for="email"> + That email address is already registered! You should try again. + </div> </li> <li id="hint_section" class="start"> diff --git a/resources/static/pages/manage_account.js b/resources/static/pages/manage_account.js index c606a69f42ade1ae55adbe5bfadcec9a25140243..0aa3a655f9ec8f5614dbf16a19b0d263cc30347d 100644 --- a/resources/static/pages/manage_account.js +++ b/resources/static/pages/manage_account.js @@ -45,7 +45,8 @@ BrowserID.manageAccount = (function() { pageHelpers = bid.PageHelpers, cancelEvent = pageHelpers.cancelEvent, confirmAction = confirm, - doc = document; + doc = document, + tooltip = bid.Tooltip; function relativeDate(date) { var diff = (((new Date()).getTime() - date.getTime()) / 1000), @@ -211,12 +212,46 @@ BrowserID.manageAccount = (function() { } } - function manageAccounts() { - $("body").addClass("edit"); + function startEdit(event) { + // XXX add some helpers in the dom library to find section. + event.preventDefault(); + $(event.target).closest("section").addClass("edit"); } - function cancelManage() { - $("body").removeClass("edit"); + function cancelEdit(event) { + event.preventDefault(); + $(event.target).closest("section").removeClass("edit"); + } + + function changePassword(oncomplete) { + var oldPassword = dom.getInner("#old_password"), + newPassword = dom.getInner("#new_password"); + + function complete(status) { + typeof oncomplete == "function" && oncomplete(status); + } + + if(!oldPassword) { + tooltip.showTooltip("#tooltipOldRequired"); + complete(false); + } + else if(!newPassword) { + tooltip.showTooltip("#tooltipNewRequired"); + complete(false); + } + else { + user.changePassword(oldPassword, newPassword, function(status) { + if(status) { + dom.removeClass("#edit_password", "edit"); + } + else { + tooltip.showTooltip("#tooltipInvalidPassword"); + } + + complete(status); + }, pageHelpers.getFailure(errors.updatePassword, oncomplete)); + } + } function displayHelpTextToNewUser() { @@ -233,8 +268,10 @@ BrowserID.manageAccount = (function() { if (options.confirm) confirmAction = options.confirm; dom.bindEvent("#cancelAccount", "click", cancelEvent(cancelAccount)); - dom.bindEvent("#manageAccounts", "click", cancelEvent(manageAccounts)); - dom.bindEvent("#cancelManage", "click", cancelEvent(cancelManage)); + + dom.bindEvent("button.edit", "click", startEdit); + dom.bindEvent("button.done", "click", cancelEdit); + dom.bindEvent("#edit_password_form", "submit", cancelEvent(changePassword)); syncAndDisplayEmails(oncomplete); @@ -244,13 +281,16 @@ BrowserID.manageAccount = (function() { // BEGIN TESTING API function reset() { dom.unbindEvent("#cancelAccount", "click"); - dom.unbindEvent("#manageAccounts", "click"); - dom.unbindEvent("#cancelManage", "click"); + + dom.unbindEvent("button.edit", "click"); + dom.unbindEvent("button.done", "click"); + dom.unbindEvent("#edit_password_form", "submit"); } init.reset = reset; init.cancelAccount = cancelAccount; init.removeEmail = removeEmail; + init.changePassword = changePassword; // END TESTING API return init; diff --git a/resources/static/pages/page_helpers.js b/resources/static/pages/page_helpers.js index c18b2817c54e3c336841f8bde097c31995ea95e2..e024ee1750279d617dedb8d3165a68403cb35f59 100644 --- a/resources/static/pages/page_helpers.js +++ b/resources/static/pages/page_helpers.js @@ -94,7 +94,7 @@ BrowserID.PageHelpers = (function() { $("#errorBackground").stop().fadeIn(); $("#error").stop().fadeIn(); - callback && callback(); + callback && callback(false); } } diff --git a/resources/static/pages/signup.js b/resources/static/pages/signup.js index fda1de0e63a143bb7c367b017764099729a359b9..a70e72fc2b558cf97f595e7b8fb060e43e0e5a51 100644 --- a/resources/static/pages/signup.js +++ b/resources/static/pages/signup.js @@ -55,45 +55,45 @@ BrowserID.signUp = (function() { function submit(oncomplete) { var email = helpers.getAndValidateEmail("#email"); - function complete() { - oncomplete && oncomplete(); + function complete(status) { + oncomplete && oncomplete(status); } if (email) { - user.addressInfo(email, function(info) { - if (info.type === 'secondary') { - if (!info.known) { - user.createUser(email, function onSuccess(success) { - if(success) { - pageHelpers.showEmailSent(oncomplete); - } - else { - tooltip.showTooltip("#could_not_add"); - complete(); - } - }, pageHelpers.getFailure(errors.createUser, oncomplete)); - } - else { + user.createUser(email, function onComplete(status) { + switch(status) { + case "secondary.already_added": $('#registeredEmail').html(email); showNotice(".alreadyRegistered"); - complete(); - } - } else { - BrowserID.Provisioning({ - email: email, - url: info.prov - }, function(r) { - // XXX: implement me - alert("shane! provisioning was a success " + JSON.stringify(r)); - }, function(e) { - // XXX: implement me - alert("shane! provisioning was a failure: " + JSON.stringify(e)); - }); + complete(false); + break; + case "secondary.verify": + pageHelpers.showEmailSent(complete); + break; + case "secondary.could_not_add": + tooltip.showTooltip("#could_not_add"); + complete(false); + break; + case "primary.already_added": + // XXX Is this status possible? + break; + case "primary.verified": + pageHelpers.replaceInputsWithNotice("#congrats", complete.bind(null, true)); + break; + case "primary.verify": + // XXX What do we do here? + complete(false); + break; + case "primary.could_not_add": + // XXX Can this happen? + break; + default: + break; } - }, pageHelpers.getFailure(errors.isEmailRegistered, oncomplete)); + }, pageHelpers.getFailure(errors.createUser, oncomplete)); } else { - complete(); + complete(false); } } @@ -105,7 +105,9 @@ BrowserID.signUp = (function() { if (event.which !== 13) $(".notification").fadeOut(ANIMATION_SPEED); } - function init() { + function init(config) { + config = config || {}; + $("form input[autofocus]").focus(); pageHelpers.setupEmail(); diff --git a/resources/static/shared/error-messages.js b/resources/static/shared/error-messages.js index f1e6cbb681922c7fce6ce5f9978b0a3094b34fa8..dc444e193b35b29526501d9ffefaae83ed91ad4d 100644 --- a/resources/static/shared/error-messages.js +++ b/resources/static/shared/error-messages.js @@ -82,6 +82,11 @@ BrowserID.Errors = (function(){ message: "Unfortunately, BrowserID cannot communicate while offline!" }, + provisioningPrimary: { + title: "Provisioning Primary", + message: "We had trouble communicating with your email provider, please try again!" + }, + registration: { title: "Registration Failed" }, @@ -119,6 +124,10 @@ BrowserID.Errors = (function(){ title: "Sync Keys for Address" }, + updatePassword: { + title: "Updating password" + }, + xhrError: { title: "Communication Error" } diff --git a/resources/static/shared/network.js b/resources/static/shared/network.js index ab3b6e96b77eb9ab3b2b8e13a875768a071b2444..16c0f60b82cedfe6118b6b9370f1129fb0ac9e84 100644 --- a/resources/static/shared/network.js +++ b/resources/static/shared/network.js @@ -120,7 +120,7 @@ BrowserID.Network = (function() { else { var url = "/wsapi/session_context"; xhr.ajax({ - url: "/wsapi/session_context", + url: url, success: function(result) { csrf_token = result.csrf_token; server_time = { @@ -379,15 +379,22 @@ BrowserID.Network = (function() { * @method changePassword * @param {string} oldpassword - old password. * @param {string} newpassword - new password. - * @param {function} [onSuccess] - Callback to call when complete. Will be + * @param {function} [onComplete] - Callback to call when complete. Will be * called with true if successful, false otw. * @param {function} [onFailure] - Called on XHR failure. */ - changePassword: function(oldPassword, newPassword, onSuccess, onFailure) { - // XXX fill this in - if (onSuccess) { - onSuccess(true); - } + changePassword: function(oldPassword, newPassword, onComplete, onFailure) { + post({ + url: "/wsapi/update_password", + data: { + oldpass: oldPassword, + newpass: newPassword + }, + success: function(response) { + if (onComplete) onComplete(response.success); + }, + error: onFailure + }); }, @@ -420,8 +427,8 @@ BrowserID.Network = (function() { email: email, site: origin }, - success: function(status) { - if (onSuccess) onSuccess(status.success); + success: function(response) { + if (onSuccess) onSuccess(response.success); }, error: function(info) { // 403 is throttling. @@ -474,7 +481,7 @@ BrowserID.Network = (function() { * (is it a primary or a secondary) * @method addressInfo * @param {string} email - Email address to check. - * @param {function} [onSuccess] - Called with an object on success, + * @param {function} [onComplete] - Called with an object on success, * containing these properties: * type: <secondary|primary> * known: boolean, present - present if type is secondary @@ -482,11 +489,11 @@ BrowserID.Network = (function() { * prov: string - url to embed for silent provisioning - present if type is secondary * @param {function} [onFailure] - Called on XHR failure. */ - addressInfo: function(email, onSuccess, onFailure) { + addressInfo: function(email, onComplete, onFailure) { get({ url: "/wsapi/address_info?email=" + encodeURIComponent(email), success: function(data, textStatus, xhr) { - if (onSuccess) onSuccess(data); + if (onComplete) onComplete(data); }, error: onFailure }); diff --git a/resources/static/shared/tooltip.js b/resources/static/shared/tooltip.js index eb512c0f90e5dbcf75d4a7f9412378640c4ee854..14d21932fee721cc290077c1f3eff982e2b52376 100644 --- a/resources/static/shared/tooltip.js +++ b/resources/static/shared/tooltip.js @@ -44,7 +44,7 @@ BrowserID.Tooltip = (function() { bid = BrowserID, dom = bid.DOM, renderer = bid.Renderer, - lastTooltip; + hideTimer; function createTooltip(el) { var contents = el.html(); @@ -74,7 +74,7 @@ BrowserID.Tooltip = (function() { bid.Tooltip.shown = true; el.fadeIn(ANIMATION_TIME, function() { - setTimeout(function() { + hideTimer = setTimeout(function() { el.fadeOut(ANIMATION_TIME, function() { bid.Tooltip.shown = false; if(complete) complete(); @@ -118,7 +118,19 @@ BrowserID.Tooltip = (function() { return { - showTooltip: showTooltip + showTooltip: showTooltip + // BEGIN TESTING API + , + reset: function() { + if(hideTimer) { + clearTimeout(hideTimer); + hideTimer = null; + } + + $(".tooltip").hide(); + bid.Tooltip.shown = false; + } + // END TESTING API }; }()); diff --git a/resources/static/shared/user.js b/resources/static/shared/user.js index a9e85b463d6b8d5c46343b536cb6c63ce1afb903..58509bb8c22d20933c9ec1623a95e182f9ba0441 100644 --- a/resources/static/shared/user.js +++ b/resources/static/shared/user.js @@ -39,9 +39,11 @@ BrowserID.User = (function() { "use strict"; var jwk, jwt, vep, jwcert, origin, - network = BrowserID.Network, - storage = BrowserID.Storage, - User, pollTimeout; + bid = BrowserID, + network = bid.Network, + storage = bid.Storage, + User, pollTimeout, + provisioning = bid.Provisioning; function prepareDeps() { if (!jwk) { @@ -52,7 +54,6 @@ BrowserID.User = (function() { } } - "use strict"; // remove identities that are no longer valid function cleanupIdentities(cb) { network.serverTime(function(serverTime) { @@ -220,6 +221,16 @@ BrowserID.User = (function() { } User = { + init: function(config) { + if(config.provisioning) { + provisioning = config.provisioning; + } + }, + + reset: function() { + provisioning = BrowserID.Provisioning; + }, + /** * Set the interface to use for networking. Used for unit testing. * @method setNetwork @@ -259,18 +270,70 @@ BrowserID.User = (function() { /** * Create a user account - this creates an user account that must be verified. - * @method createUser + * @method createSecondaryUser * @param {string} email - Email address. - * @param {function} [onSuccess] - Called on successful completion. + * @param {function} [onComplete] - Called on completion. * @param {function} [onFailure] - Called on error. */ - createUser: function(email, onSuccess, onFailure) { + createSecondaryUser: function(email, onComplete, onFailure) { var self=this; // remember this for later storage.setStagedOnBehalfOf(self.getHostname()); - network.createUser(email, origin, onSuccess, onFailure); + network.createUser(email, origin, onComplete, onFailure); + }, + + /** + * Status: + * "already_added", "verify_secondary", "secondary_could_not_add", "verify_primary", + * "primary_verified" + */ + createUser: function(email, onComplete, onFailure) { + var self=this; + + function attemptAddSecondary(email, info) { + if (info.known) { + onComplete("secondary.already_added"); + } + else { + self.createSecondaryUser(email, function(success) { + if(success) { + onComplete("secondary.verify"); + } + else { + onComplete("secondary.could_not_add"); + } + }, onFailure); + } + } + + function attemptAddPrimary(email, info) { + // XXX Can we know if the primary is already known to us? + provisioning({ + email: email, + url: info.prov + }, function(r) { + onComplete("primary.verified"); + }, function(info) { + // XXX When do we have to redirect off to the authentication page? + // Would a code like this come in on the failure mode? + if(info.code === "MUST_AUTHENTICATE") { + onComplete("primary.verify"); + } + else { + onFailure(info); + } + }); + } + + network.addressInfo(email, function(info) { + if (info.type === 'secondary') { + attemptAddSecondary(email, info); + } else { + attemptAddPrimary(email, info); + } + }, onFailure); }, /** @@ -322,7 +385,7 @@ BrowserID.User = (function() { }, /** - * Set the password of the current user. + * Set the initial password of the current user. * @method setPassword * @param {string} password - password to set * @param {function} [onComplete] - Called on successful completion. @@ -332,6 +395,20 @@ BrowserID.User = (function() { network.setPassword(password, onComplete, onFailure); }, + /** + * update the password of the current user. + * @method changePassword + * @param {string} oldpassword - the old password. + * @param {string} newpassword - the new password. + * @param {function} [onComplete] - called on completion. Called with one + * parameter, status - set to true if password update is successful, false + * otw. + * @param {function} [onFailure] - called on XHR failure. + */ + changePassword: function(oldpassword, newpassword, onComplete, onFailure) { + network.changePassword(oldpassword, newpassword, onComplete, onFailure); + }, + /** * Request a password reset for the given email address. * @method requestPasswordReset diff --git a/resources/static/test/index.html b/resources/static/test/index.html index a417d34a14e472782afe21a94151bbf3bbe0ef9d..50386e33eb941857f92ac59b623fd44155900a58 100644 --- a/resources/static/test/index.html +++ b/resources/static/test/index.html @@ -40,12 +40,10 @@ <input id="newEmail" /> <input id="password" /> <input id="vpassword" /> + <input id="old_password" /> + <input id="new_password" /> <input type="checkbox" id="remember" /> </div> - <div id="congrats">Congrats!</div> - <span id="cannotconfirm" class="error">Cannot confirm</span> - <span id="cannotcommunicate" class="error">Cannot communicate</span> - <span class="siteinfo" class="error"><span class="website"></span></span> <span class="hint">Hint</span> </div> @@ -61,8 +59,12 @@ <ul class="notifications"> + <li id="cannotconfirm" class="error notification">Cannot confirm</li> + <li id="cannotcommunicate" class="error notification">Cannot communicate</li> + <li class="siteinfo" class="error notification"><span class="website"></span></li> <li class="notification emailsent">Email Sent</li> <li class="notification doh">doh</li> + <li class="notification" id="congrats">Congratulations!</li> </ul> <ul id="emailList"> @@ -87,6 +89,7 @@ <script type="text/javascript" src="qunit/mocks/mocks.js"></script> <script type="text/javascript" src="qunit/mocks/xhr.js"></script> <script type="text/javascript" src="qunit/mocks/templates.js"></script> + <script type="text/javascript" src="qunit/mocks/provisioning.js"></script> <script type="text/javascript" src="/shared/javascript-extensions.js"></script> <script type="text/javascript" src="/shared/renderer.js"></script> <script type="text/javascript" src="/shared/class.js"></script> @@ -118,6 +121,7 @@ <script type="text/javascript" src="/dialog/controllers/authenticate.js"></script> <script type="text/javascript" src="/dialog/controllers/forgotpassword.js"></script> <script type="text/javascript" src="/dialog/controllers/required_email.js"></script> + <script type="text/javascript" src="/dialog/controllers/verify_primary_user.js"></script> <script type="text/javascript" src="/pages/page_helpers.js"></script> <script type="text/javascript" src="/pages/add_email_address.js"></script> @@ -164,7 +168,9 @@ <script type="text/javascript" src="qunit/controllers/authenticate_unit_test.js"></script> <script type="text/javascript" src="qunit/controllers/forgotpassword_unit_test.js"></script> <script type="text/javascript" src="qunit/controllers/required_email_unit_test.js"></script> - // must go last or all other tests will fail. + <script type="text/javascript" src="qunit/controllers/verify_primary_user_unit_test.js"></script> + + <!-- must go last or all other tests will fail. --> <script type="text/javascript" src="qunit/controllers/dialog_unit_test.js"></script> </body> </html> diff --git a/resources/static/test/qunit/controllers/actions_unit_test.js b/resources/static/test/qunit/controllers/actions_unit_test.js index 340c91498eeb49cf8871136a0536c6407e1592b1..5e6aee990f6d9adde622e30659599062b4f4868c 100644 --- a/resources/static/test/qunit/controllers/actions_unit_test.js +++ b/resources/static/test/qunit/controllers/actions_unit_test.js @@ -95,5 +95,27 @@ }); }); + /* + asyncTest("doVerifyPrimaryUser does something", function() { + createController({ + ready: function() { + controller.doVerifyPrimaryUser(); + // XXX test something + start(); + } + }); + }); + + asyncTest("doPrimaryUserVerified does something", function() { + createController({ + ready: function() { + controller.doPrimaryUserVerified(); + // XXX test something + start(); + } + }); + }); +*/ + }()); diff --git a/resources/static/test/qunit/controllers/authenticate_unit_test.js b/resources/static/test/qunit/controllers/authenticate_unit_test.js index 7a0d73ed193d26e88687d874f29e307b5a7326c0..60763b5fa1c6588058e6ce144b109a6e7847e223 100644 --- a/resources/static/test/qunit/controllers/authenticate_unit_test.js +++ b/resources/static/test/qunit/controllers/authenticate_unit_test.js @@ -47,7 +47,9 @@ userCreated = true, mediator = bid.Mediator, registrations = [], - register = bid.TestHelpers.register; + testHelpers = bid.TestHelpers, + register = testHelpers.register, + provisioning = bid.Mocks.Provisioning; function reset() { emailRegistered = false; @@ -63,7 +65,7 @@ module("controllers/authenticate", { setup: function() { reset(); - bid.TestHelpers.setup(); + testHelpers.setup(); createController(); }, @@ -76,7 +78,7 @@ } } reset(); - bid.TestHelpers.teardown(); + testHelpers.teardown(); } }); @@ -153,6 +155,9 @@ asyncTest("createUser with valid email", function() { $("#email").val("unregistered@testuser.com"); + + xhr.useResult("unknown_secondary"); + register("user_staged", function(msg, info) { equal(info.email, "unregistered@testuser.com", "user_staged with correct email triggered"); start(); @@ -189,8 +194,6 @@ equal(bid.Tooltip.shown, true, "tooltip is shown"); start(); }); - - }); asyncTest("createUser with valid email, XHR error", function() { diff --git a/resources/static/test/qunit/controllers/verify_primary_user_unit_test.js b/resources/static/test/qunit/controllers/verify_primary_user_unit_test.js new file mode 100644 index 0000000000000000000000000000000000000000..1a9390b0530be4443392dd51ff12f26b0a03a468 --- /dev/null +++ b/resources/static/test/qunit/controllers/verify_primary_user_unit_test.js @@ -0,0 +1,65 @@ +/*jshint browsers:true, forin: true, laxbreak: true */ +/*global 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 ***** */ +(function() { + "use strict"; + + var bid = BrowserID, + controller, + el, + testHelpers = bid.TestHelpers; + + function createController(config) { + controller = BrowserID.Modules.VerifyPrimaryUser.create(); + controller.start(config); + } + + module("controllers/verify_primary_user", { + setup: function() { + testHelpers.setup(); + }, + + teardown: function() { + if(controller) { + controller.destroy(); + } + testHelpers.teardown(); + } + }); + + // XXX Do some tests! +}()); + diff --git a/resources/static/test/qunit/mocks/mocks.js b/resources/static/test/qunit/mocks/mocks.js index 70a00e1ffc8e09b63fc58f6d70ea3fe1d21e82b4..68d2223b336ebca0c85e02153e5ee84e2f637622 100644 --- a/resources/static/test/qunit/mocks/mocks.js +++ b/resources/static/test/qunit/mocks/mocks.js @@ -1,4 +1,3 @@ - /*jshint browsers:true, forin: true, laxbreak: true */ /*global BrowserID: true */ /* ***** BEGIN LICENSE BLOCK ***** diff --git a/resources/static/test/qunit/mocks/provisioning.js b/resources/static/test/qunit/mocks/provisioning.js new file mode 100644 index 0000000000000000000000000000000000000000..70a8760e627c3de8546e748ee46cde2b4accd39f --- /dev/null +++ b/resources/static/test/qunit/mocks/provisioning.js @@ -0,0 +1,54 @@ +/*jshint browsers:true, forin: true, laxbreak: true */ +/*global 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 ***** */ +BrowserID.Mocks.Provisioning = (function() { + function Provisioning(info, onsuccess, onfailure) { + if(Provisioning.failure) onfailure(Provisioning.failure); + else onsuccess(); + } + + Provisioning.setSuccess = function(status) { + Provisioning.status = status; + }; + + Provisioning.setFailure = function(status) { + Provisioning.failure = status; + } + + return Provisioning; +}()); + + diff --git a/resources/static/test/qunit/mocks/xhr.js b/resources/static/test/qunit/mocks/xhr.js index a65e6e5bfd0a89c83b719b86e4dc151e70421679..e2f48b980ff484c45934263f4f03b531e89cbc7f 100644 --- a/resources/static/test/qunit/mocks/xhr.js +++ b/resources/static/test/qunit/mocks/xhr.js @@ -56,14 +56,8 @@ BrowserID.Mocks.xhr = (function() { var xhr = { results: { "get /wsapi/session_context valid": contextInfo, - "get /wsapi/session_context invalid": contextInfo, // We are going to test for XHR failures for session_context using - // call to serverTime. We are going to use the flag contextAjaxError - "get /wsapi/session_context ajaxError": contextInfo, - "get /wsapi/session_context complete": contextInfo, - "get /wsapi/session_context throttle": contextInfo, - "get /wsapi/session_context multiple": contextInfo, - "get /wsapi/session_context no_identities": contextInfo, + // the flag contextAjaxError. "get /wsapi/session_context contextAjaxError": undefined, "get /wsapi/email_for_token?token=token valid": { email: "testuser@testuser.com" }, "get /wsapi/email_for_token?token=token invalid": { success: false }, @@ -76,6 +70,7 @@ BrowserID.Mocks.xhr = (function() { "post /wsapi/complete_email_addition valid": { success: true }, "post /wsapi/complete_email_addition invalid": { success: false }, "post /wsapi/complete_email_addition ajaxError": undefined, + "post /wsapi/stage_user unknown_secondary": { success: true }, "post /wsapi/stage_user valid": { success: true }, "post /wsapi/stage_user invalid": { success: false }, "post /wsapi/stage_user throttle": 403, @@ -115,7 +110,19 @@ BrowserID.Mocks.xhr = (function() { "get /wsapi/list_emails no_identities": [], "get /wsapi/list_emails ajaxError": undefined, // Used in conjunction with registration to do a complete userflow - "get /wsapi/list_emails complete": {"registered@testuser.com":{}} + "get /wsapi/list_emails complete": {"registered@testuser.com":{}}, + "post /wsapi/update_password valid": { success: true }, + "post /wsapi/update_password incorrectPassword": { success: false }, + "post /wsapi/update_password invalid": undefined, + "get /wsapi/address_info?email=unregistered%40testuser.com invalid": undefined, + "get /wsapi/address_info?email=unregistered%40testuser.com throttle": { type: "secondary", known: false }, + "get /wsapi/address_info?email=unregistered%40testuser.com unknown_secondary": { type: "secondary", known: false }, + "get /wsapi/address_info?email=registered%40testuser.com known_secondary": { type: "secondary", known: true }, + "get /wsapi/address_info?email=unregistered%40testuser.com primary": { type: "primary", auth: "", prov: "" }, + "get /wsapi/address_info?email=testuser%40testuser.com unknown_secondary": { type: "secondary", known: false }, + "get /wsapi/address_info?email=testuser%40testuser.com known_secondary": { type: "secondary", known: true }, + "get /wsapi/address_info?email=testuser%40testuser.com primary": { type: "primary", auth: "", prov: "" }, + "get /wsapi/address_info?email=testuser%40testuser.com ajaxError": undefined }, setContextInfo: function(field, value) { @@ -145,7 +152,18 @@ BrowserID.Mocks.xhr = (function() { ok(false, "missing csrf token on POST request"); } - var resName = req.type + " " + req.url + " " + xhr.resultType; + + var resultType = xhr.resultType; + + // Unless the contextAjaxError is specified, use the "valid" context info. + // This makes it so we do not have to keep adding new items for + // context_info for every possible result type. + if(req.url === "/wsapi/session_context" && resultType !== "contextAjaxError") { + resultType = "valid"; + } + + var resName = req.type + " " + req.url + " " + resultType; + var result = xhr.results[resName]; var type = typeof result; diff --git a/resources/static/test/qunit/pages/manage_account_unit_test.js b/resources/static/test/qunit/pages/manage_account_unit_test.js index 1906a5919202ad69d3eefc8934f9b5e8dfa1edd5..0c18b7e66161c0e440133638c21566a354908d13 100644 --- a/resources/static/test/qunit/pages/manage_account_unit_test.js +++ b/resources/static/test/qunit/pages/manage_account_unit_test.js @@ -44,6 +44,7 @@ testHelpers = bid.TestHelpers, validToken = true, TEST_ORIGIN = "http://browserid.org", + tooltip = bid.Tooltip, mocks = { confirm: function() { return true; }, document: { location: "" } @@ -165,4 +166,73 @@ }); }); }); + + asyncTest("changePassword with missing old password, expect tooltip", function() { + bid.manageAccount(mocks, function() { + $("#old_password").val(""); + $("#new_password").val("newpassword"); + + bid.manageAccount.changePassword(function(status) { + equal(status, false, "on missing old password, status is false"); + equal(tooltip.shown, true, "tooltip is visible"); + start(); + }); + }); + }); + + asyncTest("changePassword with missing new password, expect tooltip", function() { + bid.manageAccount(mocks, function() { + $("#old_password").val("oldpassword"); + $("#new_password").val(""); + + bid.manageAccount.changePassword(function(status) { + equal(status, false, "on missing new password, status is false"); + equal(tooltip.shown, true, "tooltip is visible"); + start(); + }); + }); + }); + + asyncTest("changePassword with incorrect old password, expect tooltip", function() { + bid.manageAccount(mocks, function() { + xhr.useResult("incorrectPassword"); + + $("#old_password").val("incorrectpassword"); + $("#new_password").val("newpassword"); + + bid.manageAccount.changePassword(function(status) { + equal(status, false, "on incorrect old password, status is false"); + equal(tooltip.shown, true, "tooltip is visible"); + start(); + }); + }); + }); + + asyncTest("changePassword with XHR error, expect error message", function() { + bid.manageAccount(mocks, function() { + xhr.useResult("invalid"); + + $("#old_password").val("oldpassword"); + $("#new_password").val("newpassword"); + + bid.manageAccount.changePassword(function(status) { + equal(status, false, "on xhr error, status is false"); + start(); + }); + }); + }); + + asyncTest("changePassword happy case", function() { + bid.manageAccount(mocks, function() { + $("#old_password").val("oldpassword"); + $("#new_password").val("newpassword"); + + bid.manageAccount.changePassword(function(status) { + equal(status, true, "on proper completion, status is true"); + equal(tooltip.shown, false, "on proper completion, tooltip is not shown"); + start(); + }); + }); + }); + }()); diff --git a/resources/static/test/qunit/pages/signup_unit_test.js b/resources/static/test/qunit/pages/signup_unit_test.js index 24bf65256377ceb5371a54bcbe655530f3682324..b55022f0ad9df19b5fab4bbe3041618b34997a46 100644 --- a/resources/static/test/qunit/pages/signup_unit_test.js +++ b/resources/static/test/qunit/pages/signup_unit_test.js @@ -39,39 +39,35 @@ var bid = BrowserID, network = bid.Network, - user = bid.User, xhr = bid.Mocks.xhr, - testOrigin = "http://browserid.org"; + testOrigin = "http://browserid.org", + testHelpers = bid.TestHelpers, + provisioning = bid.Mocks.Provisioning; module("pages/signup", { setup: function() { - network.setXHR(xhr); - $(".error").removeClass("error"); - $("#error").stop().hide(); - $(".notification").stop().hide(); - xhr.useResult("valid"); - user.setOrigin(testOrigin); + testHelpers.setup(); bid.signUp(); }, teardown: function() { - network.setXHR($); - $(".error").removeClass("error"); - $("#error").stop().hide(); - $(".notification").stop().hide(); - $("#error .message").remove(); + testHelpers.teardown(); bid.signUp.reset(); } }); - function testNoticeNotVisible(extraTests) { - bid.signUp.submit(function() { + function testNotRegistered(extraTests) { + bid.signUp.submit(function(status) { + strictEqual(status, false, "address was not registered"); equal($(".emailsent").is(":visible"), false, "email not sent, notice not visible"); + if(extraTests) extraTests(); start(); }); } - asyncTest("signup with valid unregistered email", function() { + asyncTest("signup with valid unregistered secondary email", function() { + xhr.useResult("unknown_secondary"); + $("#email").val("unregistered@testuser.com"); bid.signUp.submit(function() { @@ -81,6 +77,8 @@ }); asyncTest("signup with valid unregistered email with leading/trailing whitespace", function() { + xhr.useResult("unknown_secondary"); + $("#email").val(" unregistered@testuser.com "); bid.signUp.submit(function() { @@ -90,35 +88,37 @@ }); asyncTest("signup with valid registered email", function() { + xhr.useResult("known_secondary"); $("#email").val("registered@testuser.com"); - testNoticeNotVisible(); + testNotRegistered(); }); asyncTest("signup with invalid email address", function() { $("#email").val("invalid"); - testNoticeNotVisible(); + testNotRegistered(); }); asyncTest("signup with throttling", function() { xhr.useResult("throttle"); - $("#email").val("throttled@testuser.com"); + $("#email").val("unregistered@testuser.com"); - testNoticeNotVisible(); + testNotRegistered(); }); - asyncTest("signup with invalid XHR error", function() { + asyncTest("signup with XHR error", function() { xhr.useResult("invalid"); $("#email").val("unregistered@testuser.com"); - testNoticeNotVisible(function() { - equal($("#error").is(":visible"), true, "error message displayed"); + testNotRegistered(function() { + testHelpers.testErrorVisible(); }); }); - asyncTest("signup with unregistered email and cancel button pressed", function() { + asyncTest("signup with unregistered secondary email and cancel button pressed", function() { + xhr.useResult("unknown_secondary"); $("#email").val("unregistered@testuser.com"); bid.signUp.submit(function() { @@ -131,4 +131,51 @@ }); }); + asyncTest("signup with primary email address, provisioning failure - expect error screen", function() { + xhr.useResult("primary"); + + $("#email").val("unregistered@testuser.com"); + provisioning.setFailure({ + code: "internal", + msg: "doowap" + }); + + bid.signUp.submit(function(status) { + equal(status, false, "provisioning failure, status false"); + testHelpers.testErrorVisible(); + start(); + }); + }); + + asyncTest("signup with primary email address, user verified by primary - print success message", function() { + xhr.useResult("primary"); + + $("#email").val("unregistered@testuser.com"); + + provisioning.setSuccess(true); + + bid.signUp.submit(function(status) { + equal(status, true, "primary addition success - true status"); + equal($(".notification:visible").length, 1, "success notification is visible"); + start(); + }); + }); + + // XXX what do we expect here? + asyncTest("signup with primary email address, user must verify with primary - ", function() { + xhr.useResult("primary"); + + $("#email").val("unregistered@testuser.com"); + + provisioning.setFailure({ + code: "MUST_AUTHENTICATE", + msg: "Wahhooo!!" + }); + + bid.signUp.submit(function(status) { + equal(status, false, "user must authenticate, some action needed."); + start(); + }); + }); + }()); diff --git a/resources/static/test/qunit/resources/helpers_unit_test.js b/resources/static/test/qunit/resources/helpers_unit_test.js index ed31c29bae27dad4153274d65b2c8cc189d80259..66613847707e62012465aa9686671ff3980a5edc 100644 --- a/resources/static/test/qunit/resources/helpers_unit_test.js +++ b/resources/static/test/qunit/resources/helpers_unit_test.js @@ -44,8 +44,12 @@ storage = bid.Storage, tooltip = bid.Tooltip, testHelpers = bid.TestHelpers, + user = bid.User, + provisioning = bid.Mocks.Provisioning, closeCB, - errorCB; + errorCB, + expectedError = testHelpers.expectXHRFailure, + badError = testHelpers.unexpectedXHRFailure; var controllerMock = { close: function(message, info) { @@ -61,7 +65,7 @@ function expectedClose(message, field, value) { return function(m, info) { - ok(m, message, "correct message: " + message); + equal(m, message, "correct message: " + message); if(value) { equal(info[field], value, field + " has correct value of " + value); @@ -72,14 +76,6 @@ } } - function badError() { - ok(false, "error should have never been called"); - } - - function expectedError() { - ok(true, "error condition expected"); - start(); - } function badClose() { ok(false, "close should have never been called"); @@ -90,10 +86,14 @@ testHelpers.setup(); closeCB = errorCB = null; errorCB = badError; + user.init({ + provisioning: provisioning + }); }, teardown: function() { testHelpers.teardown(); + user.reset(); } }); @@ -113,10 +113,7 @@ xhr.useResult("ajaxError"); storage.addEmail("registered@testuser.com", {}); - dialogHelpers.getAssertion.call(controllerMock, "registered@testuser.com", function() { - ok(false, "unexpected finish"); - start(); - }); + dialogHelpers.getAssertion.call(controllerMock, "registered@testuser.com", testHelpers.unexpectedSuccess); }); asyncTest("authenticateUser happy case", function() { @@ -144,7 +141,18 @@ }); }); - asyncTest("createUser happy case", function() { + asyncTest("createUser with known secondary, user not staged", function() { + closeCB = badClose; + + xhr.useResult("known_secondary"); + dialogHelpers.createUser.call(controllerMock, "registered@testuser.com", function(staged) { + equal(staged, false, "user was not staged"); + start(); + }); + }); + + asyncTest("createUser with unknown secondary happy case, expect 'user_staged' message", function() { + xhr.useResult("unknown_secondary"); closeCB = expectedClose("user_staged", "email", "unregistered@testuser.com"); dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", function(staged) { @@ -153,23 +161,45 @@ }); }); - asyncTest("createUser could not create case", function() { + asyncTest("createUser with unknown secondary, user throttled", function() { closeCB = badClose; - xhr.useResult("invalid"); - dialogHelpers.createUser.call(controllerMock, "registered@testuser.com", function(staged) { + xhr.useResult("throttle"); + dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", function(staged) { equal(staged, false, "user was not staged"); start(); }); }); - asyncTest("createUser with XHR error", function() { errorCB = expectedError; xhr.useResult("ajaxError"); - dialogHelpers.createUser.call(controllerMock, "registered@testuser.com", function(staged) { - ok(false, "complete should not have been called"); + dialogHelpers.createUser.call(controllerMock, "registered@testuser.com", testHelpers.unexpectedSuccess); + }); + + asyncTest("createUser with unknown primary, user verified - expect 'primary_user_verified' message", function() { + closeCB = expectedClose("primary_user_verified", "email", "unregistered@testuser.com"); + + xhr.useResult("primary"); + provisioning.setSuccess(true); + + dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", function(staged) { + equal(staged, true, "user was staged"); + start(); + }); + }); + + asyncTest("createUser with unknown primary, user must verify with IdP - expect 'primary_verify_user' message", function() { + closeCB = expectedClose("primary_verify_user", "email", "unregistered@testuser.com"); + + xhr.useResult("primary"); + provisioning.setFailure({ + code: "MUST_AUTHENTICATE" + }); + + dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", function(staged) { + equal(staged, true, "user was staged"); start(); }); }); diff --git a/resources/static/test/qunit/resources/state_machine_unit_test.js b/resources/static/test/qunit/resources/state_machine_unit_test.js index e988f9351decfd052811c81832a7bac1a02329c9..9453ffb7c87c54e3e0545eeee012ca833e464cd7 100644 --- a/resources/static/test/qunit/resources/state_machine_unit_test.js +++ b/resources/static/test/qunit/resources/state_machine_unit_test.js @@ -102,6 +102,14 @@ doError: function() { this.error = true; + }, + + doPrimaryUserVerified: function() { + this.primaryUserVerified = true; + }, + + doVerifyPrimaryUser: function() { + this.verifyPrimaryUser = true; } }; @@ -154,6 +162,18 @@ ok(controllerMock.emailConfirmed, "user was confirmed"); }); + // XXX make these and the messages for secondary match up so there is consistency. + test("primary_user_verified calls doPrimaryUserVerified", function() { + mediator.publish("primary_user_verified"); + + ok(controllerMock.primaryUserVerified, "doPrimaryUserVerified called"); + }); + + test("primary_verify_user calls doVerifyPrimaryUser", function() { + mediator.publish("primary_verify_user"); + ok(controllerMock.verifyPrimaryUser, "doVerifyPrimaryUser called"); + }); + test("authenticated", function() { mediator.publish("authenticated"); diff --git a/resources/static/test/qunit/shared/network_unit_test.js b/resources/static/test/qunit/shared/network_unit_test.js index d8d3fa46745c3111a52e938a9058458ebe17e288..dc7d44725e217b5756ff8f2265194e7f39f46ab3 100644 --- a/resources/static/test/qunit/shared/network_unit_test.js +++ b/resources/static/test/qunit/shared/network_unit_test.js @@ -1,5 +1,5 @@ /*jshint browsers:true, forin: true, laxbreak: true */ -/*global wrappedAsyncTest: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID: true */ +/*global asyncTest: 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 * @@ -37,22 +37,10 @@ (function() { "use strict"; - var testName, - bid = BrowserID, + var bid = BrowserID, mediator = bid.Mediator, - xhr = bid.Mocks.xhr; - - function wrappedAsyncTest(name, test) { - asyncTest(name, function() { - testName = name; - test(); - }); - } - - function wrappedStart() { - console.log("start: " + testName); - start(); - } + xhr = bid.Mocks.xhr, + testHelpers = bid.TestHelpers; function notificationCheck(cb) { // Take the original arguments, take off the function. Add any additional @@ -70,7 +58,7 @@ ok(info.network.type, "request type is in network info"); equal(info.network.textStatus, "errorStatus", "textStatus is in network info"); equal(info.network.errorThrown, "errorThrown", "errorThrown is in response info"); - wrappedStart(); + start(); mediator.unsubscribe(handle); }; @@ -81,22 +69,29 @@ } } + function unexpectedFailure() { + return function() { + ok(false, "unexpected failure"); + start(); + } + } + function failureCheck(cb) { // Take the original arguments, take off the function. Add any additional // arguments that were passed in, and then tack on the onSuccess and // onFailure to the end. Then call the callback. var args = Array.prototype.slice.call(arguments, 1); - args.push(function onSuccess(authenticated) { + args.push(function onSuccess() { ok(false, "XHR failure should never pass"); - wrappedStart(); + start(); }, function onFailure(info) { ok(true, "XHR failure should never pass"); ok(info.network.url, "url is in network info"); ok(info.network.type, "request type is in network info"); equal(info.network.textStatus, "errorStatus", "textStatus is in network info"); equal(info.network.errorThrown, "errorThrown", "errorThrown is in response info"); - wrappedStart(); + start(); }); xhr.useResult("ajaxError"); @@ -108,72 +103,71 @@ module("shared/network", { setup: function() { - network.setXHR(xhr); - xhr.useResult("valid"); + testHelpers.setup(); }, teardown: function() { - network.setXHR($); + testHelpers.teardown(); } }); - wrappedAsyncTest("authenticate with valid user", function() { + asyncTest("authenticate with valid user", function() { network.authenticate("testuser@testuser.com", "testuser", function onSuccess(authenticated) { equal(authenticated, true, "valid authentication"); - wrappedStart(); + start(); }, function onFailure() { ok(false, "valid authentication"); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("authenticate with invalid user", function() { + asyncTest("authenticate with invalid user", function() { xhr.useResult("invalid"); network.authenticate("testuser@testuser.com", "invalid", function onSuccess(authenticated) { equal(authenticated, false, "invalid authentication"); - wrappedStart(); + start(); }, function onFailure() { ok(false, "invalid authentication"); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("authenticate with XHR failure, checking whether application is notified", function() { + asyncTest("authenticate with XHR failure, checking whether application is notified", function() { notificationCheck(network.authenticate, "testuser@testuser.com", "ajaxError"); }); - wrappedAsyncTest("authenticate with XHR failure after context already setup", function() { + asyncTest("authenticate with XHR failure after context already setup", function() { failureCheck(network.authenticate, "testuser@testuser.com", "ajaxError"); }); - wrappedAsyncTest("checkAuth with valid authentication", function() { + asyncTest("checkAuth with valid authentication", function() { xhr.setContextInfo("authenticated", true); network.checkAuth(function onSuccess(authenticated) { equal(authenticated, true, "we have an authentication"); - wrappedStart(); + start(); }, function onFailure() { ok(false, "checkAuth failure"); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("checkAuth with invalid authentication", function() { + asyncTest("checkAuth with invalid authentication", function() { xhr.useResult("invalid"); xhr.setContextInfo("authenticated", false); network.checkAuth(function onSuccess(authenticated) { equal(authenticated, false, "we are not authenticated"); - wrappedStart(); + start(); }, function onFailure() { ok(false, "checkAuth failure"); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("checkAuth with XHR failure", function() { + asyncTest("checkAuth with XHR failure", function() { xhr.useResult("ajaxError"); xhr.setContextInfo("authenticated", false); @@ -182,428 +176,399 @@ // request, we do not test whether the app is notified of an XHR failure network.checkAuth(function onSuccess() { ok(true, "checkAuth does not make an ajax call, all good"); - wrappedStart(); + start(); }, function onFailure() { ok(false, "checkAuth does not make an ajax call, should not fail"); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("logout", function() { + asyncTest("logout", function() { network.logout(function onSuccess() { ok(true, "we can logout"); - wrappedStart(); + start(); }, function onFailure() { ok(false, "logout failure"); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("logout with XHR failure", function() { + asyncTest("logout with XHR failure", function() { notificationCheck(network.logout); }); - wrappedAsyncTest("logout with XHR failure", function() { + asyncTest("logout with XHR failure", function() { failureCheck(network.logout); }); - wrappedAsyncTest("complete_email_addition valid", function() { + asyncTest("complete_email_addition valid", function() { network.completeEmailRegistration("goodtoken", function onSuccess(proven) { equal(proven, true, "good token proved"); - wrappedStart(); + start(); }, function onFailure() { - wrappedStart(); + start(); }); }); - wrappedAsyncTest("complete_email_addition with invalid token", function() { + asyncTest("complete_email_addition with invalid token", function() { xhr.useResult("invalid"); network.completeEmailRegistration("badtoken", function onSuccess(proven) { equal(proven, false, "bad token could not be proved"); - wrappedStart(); + start(); }, function onFailure() { - wrappedStart(); + start(); }); }); - wrappedAsyncTest("complete_email_addition with XHR failure", function() { + asyncTest("complete_email_addition with XHR failure", function() { notificationCheck(network.completeEmailRegistration, "goodtoken"); }); - wrappedAsyncTest("complete_email_addition with XHR failure", function() { + asyncTest("complete_email_addition with XHR failure", function() { failureCheck(network.completeEmailRegistration, "goodtoken"); }); - wrappedAsyncTest("createUser with valid user", function() { + asyncTest("createUser with valid user", function() { network.createUser("validuser", "origin", function onSuccess(created) { ok(created); - wrappedStart(); + start(); }, function onFailure() { - wrappedStart(); + start(); }); }); - wrappedAsyncTest("createUser with invalid user", function() { + asyncTest("createUser with invalid user", function() { xhr.useResult("invalid"); network.createUser("invaliduser", "origin", function onSuccess(created) { equal(created, false); - wrappedStart(); + start(); }, function onFailure() { - wrappedStart(); + start(); }); }); - wrappedAsyncTest("createUser throttled", function() { + asyncTest("createUser throttled", function() { xhr.useResult("throttle"); network.createUser("validuser", "origin", function onSuccess(added) { equal(added, false, "throttled email returns onSuccess but with false as the value"); - wrappedStart(); + start(); }, function onFailure() { ok(false); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("createUser with XHR failure", function() { + asyncTest("createUser with XHR failure", function() { notificationCheck(network.createUser, "validuser", "origin"); }); - wrappedAsyncTest("createUser with XHR failure", function() { + asyncTest("createUser with XHR failure", function() { failureCheck(network.createUser, "validuser", "origin"); }); - wrappedAsyncTest("checkUserRegistration with pending email", function() { + asyncTest("checkUserRegistration with pending email", function() { xhr.useResult("pending"); network.checkUserRegistration("registered@testuser.com", function(status) { equal(status, "pending"); - wrappedStart(); + start(); }, function onFailure() { ok(false); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("checkUserRegistration with complete email", function() { + asyncTest("checkUserRegistration with complete email", function() { xhr.useResult("complete"); network.checkUserRegistration("registered@testuser.com", function(status) { equal(status, "complete"); - wrappedStart(); + start(); }, function onFailure() { ok(false); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("checkUserRegistration with XHR failure", function() { + asyncTest("checkUserRegistration with XHR failure", function() { notificationCheck(network.checkUserRegistration, "registered@testuser.com"); }); - wrappedAsyncTest("checkUserRegistration with XHR failure", function() { + asyncTest("checkUserRegistration with XHR failure", function() { failureCheck(network.checkUserRegistration, "registered@testuser.com"); }); - wrappedAsyncTest("completeUserRegistration with valid token", function() { + asyncTest("completeUserRegistration with valid token", function() { network.completeUserRegistration("token", "password", function(registered) { ok(registered); - wrappedStart(); + start(); }, function onFailure() { ok(false); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("completeUserRegistration with invalid token", function() { + asyncTest("completeUserRegistration with invalid token", function() { xhr.useResult("invalid"); network.completeUserRegistration("token", "password", function(registered) { equal(registered, false); - wrappedStart(); + start(); }, function onFailure() { ok(false); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("completeUserRegistration with XHR failure", function() { + asyncTest("completeUserRegistration with XHR failure", function() { notificationCheck(network.completeUserRegistration, "token", "password"); }); - wrappedAsyncTest("completeUserRegistration with XHR failure", function() { + asyncTest("completeUserRegistration with XHR failure", function() { failureCheck(network.completeUserRegistration, "token", "password"); }); - wrappedAsyncTest("cancelUser valid", function() { + asyncTest("cancelUser valid", function() { network.cancelUser(function() { // XXX need a test here. ok(true); - wrappedStart(); + start(); }, function onFailure() { - wrappedStart(); + start(); }); }); - wrappedAsyncTest("cancelUser invalid", function() { + asyncTest("cancelUser invalid", function() { xhr.useResult("invalid"); network.cancelUser(function() { // XXX need a test here. ok(true); - wrappedStart(); + start(); }, function onFailure() { - wrappedStart(); + start(); }); }); - wrappedAsyncTest("cancelUser with XHR failure", function() { + asyncTest("cancelUser with XHR failure", function() { notificationCheck(network.cancelUser); }); - wrappedAsyncTest("cancelUser with XHR failure", function() { + asyncTest("cancelUser with XHR failure", function() { failureCheck(network.cancelUser); }); - wrappedAsyncTest("emailRegistered with taken email", function() { + asyncTest("emailRegistered with taken email", function() { network.emailRegistered("registered@testuser.com", function(taken) { equal(taken, true, "a taken email is marked taken"); - wrappedStart(); + start(); }, function onFailure() { ok(false); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("emailRegistered with nottaken email", function() { + asyncTest("emailRegistered with nottaken email", function() { network.emailRegistered("unregistered@testuser.com", function(taken) { equal(taken, false, "a not taken email is not marked taken"); - wrappedStart(); + start(); }, function onFailure() { ok(false); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("emailRegistered with XHR failure", function() { + asyncTest("emailRegistered with XHR failure", function() { notificationCheck(network.emailRegistered, "registered@testuser.com"); }); - wrappedAsyncTest("emailRegistered with XHR failure", function() { + asyncTest("emailRegistered with XHR failure", function() { failureCheck(network.emailRegistered, "registered@testuser.com"); }); - wrappedAsyncTest("addEmail valid", function() { + asyncTest("addEmail valid", function() { network.addEmail("address", "origin", function onSuccess(added) { ok(added); - wrappedStart(); + start(); }, function onFailure() { ok(false); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("addEmail invalid", function() { + asyncTest("addEmail invalid", function() { xhr.useResult("invalid"); network.addEmail("address", "origin", function onSuccess(added) { equal(added, false); - wrappedStart(); + start(); }, function onFailure() { ok(false); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("addEmail throttled", function() { + asyncTest("addEmail throttled", function() { xhr.useResult("throttle"); network.addEmail("address", "origin", function onSuccess(added) { equal(added, false, "throttled email returns onSuccess but with false as the value"); - wrappedStart(); + start(); }, function onFailure() { ok(false); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("addEmail with XHR failure", function() { + asyncTest("addEmail with XHR failure", function() { notificationCheck(network.addEmail, "address", "origin"); }); - wrappedAsyncTest("addEmail with XHR failure", function() { + asyncTest("addEmail with XHR failure", function() { failureCheck(network.addEmail, "address", "origin"); }); - wrappedAsyncTest("checkEmailRegistration pending", function() { + asyncTest("checkEmailRegistration pending", function() { xhr.useResult("pending"); network.checkEmailRegistration("registered@testuser.com", function(status) { equal(status, "pending"); - wrappedStart(); + start(); }, function onFailure() { ok(false); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("checkEmailRegistration complete", function() { + asyncTest("checkEmailRegistration complete", function() { xhr.useResult("complete"); network.checkEmailRegistration("registered@testuser.com", function(status) { equal(status, "complete"); - wrappedStart(); + start(); }, function onFailure() { ok(false); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("checkEmailRegistration with XHR failure", function() { + asyncTest("checkEmailRegistration with XHR failure", function() { notificationCheck(network.checkEmailRegistration, "address"); }); - wrappedAsyncTest("checkEmailRegistration with XHR failure", function() { + asyncTest("checkEmailRegistration with XHR failure", function() { failureCheck(network.checkEmailRegistration, "address"); }); - wrappedAsyncTest("removeEmail valid", function() { + asyncTest("removeEmail valid", function() { network.removeEmail("validemail", function onSuccess() { // XXX need a test here; ok(true); - wrappedStart(); + start(); }, function onFailure() { ok(false); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("removeEmail invalid", function() { + asyncTest("removeEmail invalid", function() { xhr.useResult("invalid"); network.removeEmail("invalidemail", function onSuccess() { // XXX need a test here; ok(true); - wrappedStart(); + start(); }, function onFailure() { ok(false); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("removeEmail with XHR failure", function() { + asyncTest("removeEmail with XHR failure", function() { notificationCheck(network.removeEmail, "validemail"); }); - wrappedAsyncTest("removeEmail with XHR failure", function() { + asyncTest("removeEmail with XHR failure", function() { failureCheck(network.removeEmail, "invalidemail"); }); - wrappedAsyncTest("requestPasswordReset", function() { + asyncTest("requestPasswordReset", function() { network.requestPasswordReset("address", "origin", function onSuccess() { // XXX need a test here; ok(true); - wrappedStart(); + start(); }, function onFailure() { ok(false); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("requestPasswordReset with XHR failure", function() { + asyncTest("requestPasswordReset with XHR failure", function() { notificationCheck(network.requestPasswordReset, "address", "origin"); }); - wrappedAsyncTest("requestPasswordReset with XHR failure", function() { + asyncTest("requestPasswordReset with XHR failure", function() { failureCheck(network.requestPasswordReset, "address", "origin"); }); - wrappedAsyncTest("resetPassword", function() { + asyncTest("resetPassword", function() { network.resetPassword("password", function onSuccess() { // XXX need a test here; ok(true); - wrappedStart(); + start(); }, function onFailure() { ok(false); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("resetPassword with XHR failure", function() { + asyncTest("resetPassword with XHR failure", function() { xhr.useResult("ajaxError"); /* the body of this function is not yet written network.resetPassword("password", function onSuccess() { ok(false, "XHR failure should never call success"); - wrappedStart(); + start(); }, function onFailure() { ok(true, "XHR failure should always call failure"); - wrappedStart(); + start(); }); */ start(); }); - wrappedAsyncTest("changePassword", function() { - network.changePassword("oldpassword", "newpassword", function onSuccess() { - // XXX need a real wrappedAsyncTest here. - ok(true); - wrappedStart(); - }, function onFailure() { - ok(false); - wrappedStart(); - }); - - }); - - wrappedAsyncTest("changePassword with XHR failure", function() { - xhr.useResult("ajaxError"); - - /* - the body of this function is not yet written. - network.changePassword("oldpassword", "newpassword", function onSuccess() { - ok(false, "XHR failure should never call success"); - wrappedStart(); - }, function onFailure() { - ok(true, "XHR failure should always call failure"); - wrappedStart(); - }); - - */ - start(); - }); - - wrappedAsyncTest("serverTime", function() { + asyncTest("serverTime", function() { // I am forcing the server time to be 1.25 seconds off. xhr.setContextInfo("server_time", new Date().getTime() - 1250); network.serverTime(function onSuccess(time) { @@ -613,53 +578,122 @@ // time as it is on the server, which could be more than 100ms off of // what the local machine says it is. //equal(Math.abs(diff) < 100, true, "server time and local time should be less than 100ms different (is " + diff + "ms different)"); - wrappedStart(); + start(); }, function onfailure() { - wrappedStart(); + start(); }); }); - wrappedAsyncTest("serverTime with XHR failure before context has been setup", function() { + asyncTest("serverTime with XHR failure before context has been setup", function() { notificationCheck(); xhr.useResult("contextAjaxError"); network.serverTime(); }); - wrappedAsyncTest("serverTime with XHR failure before context has been setup", function() { + asyncTest("serverTime with XHR failure before context has been setup", function() { xhr.useResult("contextAjaxError"); network.serverTime(function onSuccess(time) { ok(false, "XHR failure should never call success"); - wrappedStart(); + start(); }, function onFailure() { ok(true, "XHR failure should always call failure"); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("codeVersion", function() { + asyncTest("codeVersion", function() { network.codeVersion(function onComplete(version) { equal(version, "ABC123", "version returned properly"); - wrappedStart(); + start(); }, function onFailure() { ok(false, "unexpected failure"); - wrappedStart(); + start(); }); }); - wrappedAsyncTest("codeVersion with XHR error", function() { + asyncTest("codeVersion with XHR error", function() { xhr.useResult("contextAjaxError"); network.codeVersion(function onComplete(version) { ok(false, "XHR failure should never call complete"); - wrappedStart(); + start(); }, function onFailure() { - ok(true, "XHR fialure should always return failure"); - wrappedStart(); + ok(true, "XHR failure should always return failure"); + start(); }); + }); + + asyncTest("addressInfo with unknown secondary email", function() { + xhr.useResult("unknown_secondary"); + network.addressInfo("testuser@testuser.com", function onComplete(data) { + equal(data.type, "secondary", "type is secondary"); + equal(data.known, false, "address is unknown to BrowserID"); + start(); + }, unexpectedFailure); + }); + + asyncTest("addressInfo with known seconday email", function() { + xhr.useResult("known_secondary"); + + network.addressInfo("testuser@testuser.com", function onComplete(data) { + equal(data.type, "secondary", "type is secondary"); + equal(data.known, true, "address is known to BrowserID"); + start(); + }, unexpectedFailure); + }); + + asyncTest("addressInfo with primary email", function() { + xhr.useResult("primary"); + + network.addressInfo("testuser@testuser.com", function onComplete(data) { + equal(data.type, "primary", "type is primary"); + ok("auth" in data, "auth field exists"); + ok("prov" in data, "prov field exists"); + start(); + }, unexpectedFailure); + }); + + asyncTest("addressInfo with XHR error", function() { + xhr.useResult("ajaxError"); + failureCheck(network.addressInfo, "testuser@testuser.com"); + }); + + asyncTest("changePassword happy case, expect complete callback with true status", function() { + network.changePassword("oldpassword", "newpassword", function onComplete(status) { + equal(status, true, "calls onComplete with true status"); + start(); + }, function onFailure() { + ok(false, "unexpected failure"); + start(); + }); + }); + + asyncTest("changePassword with incorrect old password, expect complete callback with false status", function() { + xhr.useResult("incorrectPassword"); + + network.changePassword("oldpassword", "newpassword", function onComplete(status) { + equal(status, false, "calls onComplete with false status"); + start(); + }, function onFailure() { + ok(false, "unexpected failure"); + start(); + }); + }); + + asyncTest("changePassword with XHR error, expect error callback", function() { + xhr.useResult("ajaxError"); + + network.changePassword("oldpassword", "newpassword", function onComplete() { + ok(false, "XHR failure should never call complete"); + start(); + }, function onFailure() { + ok(true, "XHR failure should always call failure"); + start(); + }); }); }()); diff --git a/resources/static/test/qunit/shared/tooltip_unit_test.js b/resources/static/test/qunit/shared/tooltip_unit_test.js index 6c5d7e359aa030f9675044e89f58296794d22bb5..1c3bb4065c284e2c291e615155157e235cfdea0e 100644 --- a/resources/static/test/qunit/shared/tooltip_unit_test.js +++ b/resources/static/test/qunit/shared/tooltip_unit_test.js @@ -76,4 +76,15 @@ }); }); + asyncTest("show tooltip, then reset - hides tooltip, resets shown status", function() { + tooltip.showTooltip("#shortTooltip"); + setTimeout(function() { + tooltip.reset(); + + equal($(".tooltip:visible").length, 0, "after reset, all tooltips are hidden"); + equal(tooltip.shown, false, "after reset, tooltip status is reset"); + start(); + }, 100); + }); + }()); diff --git a/resources/static/test/qunit/shared/user_unit_test.js b/resources/static/test/qunit/shared/user_unit_test.js index e90fd1c32e4a82ffb971c2538566a94fe5ff710c..997c7239960f445f3d66f4f7789125de7a629f9b 100644 --- a/resources/static/test/qunit/shared/user_unit_test.js +++ b/resources/static/test/qunit/shared/user_unit_test.js @@ -44,7 +44,9 @@ var jwcert = require("./jwcert"); storage = bid.Storage, network = bid.Network, xhr = bid.Mocks.xhr, - testOrigin = "testOrigin"; + testOrigin = "https://browserid.org", + testHelpers = bid.TestHelpers, + provisioning = bid.Mocks.Provisioning // I generated these locally, they are used nowhere else. var pubkey = {"algorithm":"RS","n":"56063028070432982322087418176876748072035482898334811368408525596198252519267108132604198004792849077868951906170812540713982954653810539949384712773390200791949565903439521424909576832418890819204354729217207360105906039023299561374098942789996780102073071760852841068989860403431737480182725853899733706069","e":"65537"}; @@ -92,14 +94,11 @@ var jwcert = require("./jwcert"); module("shared/user", { setup: function() { - network.setXHR(xhr); - xhr.useResult("valid"); - lib.clearStoredEmailKeypairs(); + testHelpers.setup(); lib.setOrigin(testOrigin); - storage.site.remove(testOrigin, "email"); }, teardown: function() { - network.setXHR($); + testHelpers.teardown(); } }); @@ -116,11 +115,11 @@ var jwcert = require("./jwcert"); }); test("setOrigin, getHostname", function() { - var origin = "http://testorigin.com:10001"; + var origin = "http://browserid.org"; lib.setOrigin(origin); var hostname = lib.getHostname(); - equal(hostname, "testorigin.com", "getHostname returns only the hostname"); + equal(hostname, "browserid.org", "getHostname returns only the hostname"); }); test("getStoredEmailKeypairs", function() { @@ -156,6 +155,7 @@ var jwcert = require("./jwcert"); equal(0, count, "after clearing, there are no identities"); }); + /* asyncTest("createUser", function() { lib.createUser("testuser@testuser.com", function(status) { ok(status, "user created"); @@ -183,6 +183,70 @@ var jwcert = require("./jwcert"); start(); }); }); +*/ + asyncTest("createUser with unknown secondary happy case - expect 'secondary.verify'", function() { + xhr.useResult("unknown_secondary"); + + lib.createUser("unregistered@testuser.com", function(status) { + equal(status, "secondary.verify", "secondary user must be verified"); + start(); + }, failure("createUser failure")); + }); + + asyncTest("createUser with unknown secondary, throttled - expect status='secondary.could_not_add'", function() { + xhr.useResult("throttle"); + + lib.createUser("unregistered@testuser.com", function(status) { + equal(status, "secondary.could_not_add", "user creation refused"); + start(); + }, failure("createUser failure")); + }); + + asyncTest("createUser with unknown secondary, XHR failure - expect failure call", function() { + xhr.useResult("ajaxError"); + + lib.createUser("unregistered@testuser.com", + testHelpers.unexpectedSuccess, + testHelpers.expectXHRFailure + ); + }); + + asyncTest("createUser with primary, user verified with primary - expect 'primary.verified'", function() { + xhr.useResult("primary"); + provisioning.setSuccess(true); + + lib.createUser("unregistered@testuser.com", function(status) { + equal(status, "primary.verified", "primary user is already verified, correct status"); + start(); + }, testHelpers.unexpectedXHRFailure); + }); + + asyncTest("createUser with primary, user must authenticate with primary - expect 'primary.verify'", function() { + xhr.useResult("primary"); + + provisioning.setFailure({ + code: "MUST_AUTHENTICATE", + msg: "Wahhooo!!" + }); + + lib.createUser("unregistered@testuser.com", function(status) { + equal(status, "primary.verify", "primary must verify with primary, correct status"); + start(); + }, testHelpers.unexpectedXHRFailure); + }); + + asyncTest("createUser with primary, unknown provisioning failure, expect XHR failure callback", function() { + xhr.useResult("primary"); + provisioning.setFailure({ + code: "primaryError", + msg: "some error" + }); + + lib.createUser("unregistered@testuser.com", + testHelpers.unexpectedSuccess, + testHelpers.expectXHRFailure + ); + }); asyncTest("waitForUserValidation with `complete` response", function() { storage.setStagedOnBehalfOf(testOrigin); diff --git a/resources/static/test/qunit/testHelpers/helpers.js b/resources/static/test/qunit/testHelpers/helpers.js index d7ca0c676660680ba8737649b1fa02ba827b4cfc..61890240ceafa51f74c34371e1cf8bbd859d0c04 100644 --- a/resources/static/test/qunit/testHelpers/helpers.js +++ b/resources/static/test/qunit/testHelpers/helpers.js @@ -2,11 +2,15 @@ var bid = BrowserID, mediator = bid.Mediator, network = bid.Network, + user = bid.User, storage = bid.Storage, xhr = bid.Mocks.xhr, + provisioning = bid.Mocks.Provisioning, screens = bid.Screens, + tooltip = bid.Tooltip, registrations = []; - calls = {}; + calls = {}, + testOrigin = "https://browserid.org"; function register(message, cb) { registrations.push(mediator.subscribe(message, function(msg, info) { @@ -43,12 +47,20 @@ var el = $("#controller_head"); el.find("#formWrap .contents").html(""); el.find("#wait .contents").html(""); - $("#error").html("<div class='contents'></div>").hide(); - + $(".error").removeClass("error"); + $("#error").stop().html("<div class='contents'></div>").hide(); + $(".notification").stop().hide(); unregisterAll(); mediator.reset(); screens.wait.hide(); screens.error.hide(); + tooltip.reset(); + provisioning.setSuccess(false); + provisioning.setFailure(false); + user.init({ + provisioning: provisioning + }); + user.setOrigin(testOrigin); }, teardown: function() { @@ -56,15 +68,38 @@ mediator.reset(); network.setXHR($); storage.clear(); - $("#error").html("<div class='contents'></div>").hide(); + $(".error").removeClass("error"); + $("#error").stop().html("<div class='contents'></div>").hide(); + $(".notification").stop().hide(); screens.wait.hide(); screens.error.hide(); + tooltip.reset(); + provisioning.setSuccess(false); + provisioning.setFailure(false); + user.reset(); }, register: register, errorVisible: function() { return screens.error.visible; }, - checkNetworkError: checkNetworkError + testErrorVisible: function() { + equal(this.errorVisible(), true, "error screen is visible"); + }, + checkNetworkError: checkNetworkError, + unexpectedSuccess: function() { + ok(false, "unexpected success"); + start(); + }, + + expectXHRFailure: function() { + ok(true, "expected XHR failure"); + start(); + }, + + unexpectedXHRFailure: function() { + ok(false, "unexpected XHR failure"); + start(); + } }; }()); diff --git a/resources/views/dialog_layout.ejs b/resources/views/dialog_layout.ejs index fb7412ef13a561e9eefa87e4dd3cc1f29e042749..52fa963ec241e7d4763f6b181a4940da0f1aba5f 100644 --- a/resources/views/dialog_layout.ejs +++ b/resources/views/dialog_layout.ejs @@ -91,6 +91,7 @@ <script type="text/javascript" src="/dialog/controllers/pickemail.js"></script> <script type="text/javascript" src="/dialog/controllers/addemail.js"></script> <script type="text/javascript" src="/dialog/controllers/required_email.js"></script> + <script type="text/javascript" src="/dialog/controllers/verify_primary_user.js"></script> <script type="text/javascript" src="/dialog/start.js"></script> <% } %> <% } %> diff --git a/resources/views/index.ejs b/resources/views/index.ejs index 46cae5c26fa0a3bfe6856940f8437bff1614f065..9d9aea739112578bb45d68478ad9551920e336e3 100644 --- a/resources/views/index.ejs +++ b/resources/views/index.ejs @@ -5,15 +5,45 @@ <div id="manage"> <h1 class="serif">Account Manager</h1> - <div class="buttonrow cf"> - <strong>Your Email Addresses</strong> - - <button id="manageAccounts" href="#">edit</button> - <button id="cancelManage" href="#">done</button> - </div> - <ul id="emailList"> - </ul> - <div id="disclaimer">You may, at any time, <a href="#" id="cancelAccount">cancel your account</a></div> + + <section> + <header class="buttonrow cf"> + <h2>Your Email Addresses</h2> + + <button class="edit">edit</button> + <button class="done">done</button> + </header> + + <ul id="emailList"> + </ul> + </section> + + <section id="edit_password"> + <header class="buttonrow cf"> + <h2>Password</h2> + + <button class="edit">edit</button> + <button class="done">cancel</button> + </header> + + <div class="showedit"> + <label for="old_password">Old Password</label> + <label for="new_password">New Password</label> + </div> + + <form id="edit_password_form" class="showedit"> + <input type="password" id="old_password" name="old_password" placeholder="old password"/> + <input type="password" id="new_password" name="new_password" placeholder="new password"/> + <button id="changePassword">done</button> + + <div class="tooltip" for="old_password" id="tooltipOldRequired">Old password is required</div> + <div class="tooltip" for="old_password" id="tooltipInvalidPassword">Incorrect old password, password not updated</div> + <div class="tooltip" for="new_password" id="tooltipNewRequired">New password is required</div> + </form> + </section> + + + <p id="disclaimer">You may, at any time, <a href="#" id="cancelAccount">cancel your account</a></p> </div> </div> diff --git a/resources/views/signup.ejs b/resources/views/signup.ejs index 1578755fa2ee71c89e922758b348b32cb5e96e8b..26997d3c19ce02eb7168f3e8cf07bb93157c27fe 100644 --- a/resources/views/signup.ejs +++ b/resources/views/signup.ejs @@ -27,6 +27,15 @@ </p> </li> + <li class="notification" id="congrats"> + <p class="serif"> + <strong id="email">Your address</strong> has been verified! + + <p class="siteinfo"> + Your new address is set up! + </p> + </p> + </li> </ul> <ul class="inputs forminputs">