diff --git a/resources/static/dialog/controllers/is_this_your_computer.js b/resources/static/dialog/controllers/is_this_your_computer.js index 120c5ca1ef6b6899e32ce88d38d8cb63980745f2..1c2bc2c9eb3599f62239f7130a074f89f640d672 100644 --- a/resources/static/dialog/controllers/is_this_your_computer.js +++ b/resources/static/dialog/controllers/is_this_your_computer.js @@ -16,16 +16,13 @@ BrowserID.Modules.IsThisYourComputer = (function() { var Module = bid.Modules.PageModule.extend({ start: function(options) { options = options || {}; - email = options.email; - var self = this, - complete = function(status) { - options.ready && options.ready(status || false); - }; + var self = this; self.renderWait("is_this_your_computer", options); + // TODO - Make the selectors use ids instead of classes. self.click("button.this_is_my_computer", self.yes); self.click("button.this_is_not_my_computer", self.no); @@ -33,13 +30,19 @@ BrowserID.Modules.IsThisYourComputer = (function() { }, yes: function() { + // TODO - Move this to user.js where it could be used by other clients in + // other areas. storage.usersComputer.setConfirmed(network.userid()); - this.publish("email_chosen", { email: email }); + this.confirmed(true); }, no: function() { storage.usersComputer.setDenied(network.userid()); - this.publish("email_chosen", { email: email }); + this.confirmed(false); + }, + + confirmed: function(status) { + this.publish("user_computer_status_set", { users_computer: status }); } }); diff --git a/resources/static/shared/network.js b/resources/static/shared/network.js index ce4380fbafb48951c526fbd35e8f7767bf49b2d9..cbaf2c135f4bab2c558b985ff556c09526c714b0 100644 --- a/resources/static/shared/network.js +++ b/resources/static/shared/network.js @@ -67,6 +67,9 @@ BrowserID.Network = (function() { // this is used to key off client side state, like whether this user has // confirmed ownership of this device userid = status.userid; + if (userid) { + storage.usersComputer.setSeen(userid); + } // at this point we know the authentication status of the // session, let's set it to perhaps save a network request diff --git a/resources/static/shared/storage.js b/resources/static/shared/storage.js index ccdeb53d33317bb3c922faeb82f58e2b4f05e19d..b25b65af0c3c29ee472cf29a9ae9ddf094bc58ad 100644 --- a/resources/static/shared/storage.js +++ b/resources/static/shared/storage.js @@ -183,10 +183,12 @@ BrowserID.Storage = (function() { else delete allInfo[origin]; storage.loggedIn = JSON.stringify(allInfo); } + function getLoggedIn(origin) { var allInfo = JSON.parse(storage.loggedIn || "{}"); return allInfo[origin]; } + function watchLoggedIn(origin, callback) { var lastState = getLoggedIn(origin); @@ -217,7 +219,7 @@ BrowserID.Storage = (function() { function validState(state) { return (state === 'seen' || state === 'confirmed' || state === 'denied'); } - + function setConfirmationState(userid, state) { userid = mapEmailToUserID(userid); @@ -227,13 +229,17 @@ BrowserID.Storage = (function() { var allInfo; var currentState; - var lastUpdated = 0; + var lastUpdated = 0; + try { allInfo = JSON.parse(storage.usersComputer); if (typeof allInfo !== 'object') throw 'bogus'; - if (allInfo[userid]) { - currentState = allInfo[userid][0]; - lastUpdated = Date.parse(allInfo[userid][1]); + + var userInfo = allInfo[userid]; + if (userInfo) { + currentState = userInfo.state; + lastUpdated = Date.parse(userInfo.updated); + if (!validState(currentState)) throw "corrupt/outdated"; if (NaN === lastUpdated) throw "corrupt/outdated"; } @@ -242,7 +248,7 @@ BrowserID.Storage = (function() { lastUpdated = 0; allInfo = {}; } - + // ...now determine if we should update the state... // first if the user said this wasn't their computer over 24 hours ago, @@ -256,21 +262,22 @@ BrowserID.Storage = (function() { // if the user has a non-null state and this is another user sighting // (seen), then forget it if (state === 'seen' && currentState) return; - + // good to go! let's make the update - allInfo[userid] = [ state, new Date().toString() ]; - storage.usersComputer = JSON.stringify(allInfo); + allInfo[userid] = {state: state, updated: new Date().toString()}; + storage.usersComputer = JSON.stringify(allInfo); } function userConfirmedOnComputer(userid) { try { userid = mapEmailToUserID(userid); var allInfo = JSON.parse(storage.usersComputer || "{}"); - return allInfo[userid][0] === 'confirmed'; + return allInfo[userid].state === 'confirmed'; } catch(e) { return false; - } + } } + function shouldAskUserAboutHerComputer(userid) { // we should ask the user if this is their computer if they were // first seen over a minute ago, if they haven't denied ownership @@ -279,29 +286,46 @@ BrowserID.Storage = (function() { try { userid = mapEmailToUserID(userid); var allInfo = JSON.parse(storage.usersComputer); - - var s = allInfo[userid][0]; - var timeago = new Date() - Date.parse(allInfo[userid][1]); + var userInfo = allInfo[userid]; + var s = userInfo.state; + var timeago = new Date() - Date.parse(userInfo.updated); if (s === 'confirmed') return false; if (s === 'denied' && timeago > ONE_DAY_IN_MS) return true; - if (s === 'seen' && timeago > (60 * 1000)) return true; + if (s === 'seen' && timeago > (5 * 1000)) return true; } catch (e) { return true; } return false; } + function setUserSeenOnComputer(userid) { setConfirmationState(userid, 'seen'); } + function setUserConfirmedOnComputer(userid) { setConfirmationState(userid, 'confirmed'); } + function setNotMyComputer(userid) { setConfirmationState(userid, 'denied'); } + function clearUsersComputerOwnershipStatus(userid) { + try { + allInfo = JSON.parse(storage.usersComputer); + if (typeof allInfo !== 'object') throw 'bogus'; + + var userInfo = allInfo[userid]; + if (userInfo) { + allInfo[userid] = null; + delete allInfo[userid]; + storage.usersComputer = JSON.stringify(allInfo); + } + } catch (e) {} + } + // update our local storage based mapping of email addresses to userids, // this map helps us determine whether a specific email address belongs // to a user who has already confirmed their ownership of a computer. @@ -380,30 +404,40 @@ BrowserID.Storage = (function() { }, usersComputer: { - /** Query whether the user has confirmed that this is their computer + /** + * Query whether the user has confirmed that this is their computer * @param {integer} userid - the user's numeric id, returned from session_context when authed. * @method usersComputer.confirmed */ confirmed: userConfirmedOnComputer, - /** Save the fact that a user confirmed that this is their computer + /** + * Save the fact that a user confirmed that this is their computer * @param {integer} userid - the user's numeric id, returned from session_context when authed. * @method usersComputer.setConfirmed */ setConfirmed: setUserConfirmedOnComputer, - /** Save the fact that a user denied that this is their computer + /** + * Save the fact that a user denied that this is their computer * @param {integer} userid - the user's numeric id, returned from session_context when authed. * @method usersComputer.setDenied */ setDenied: setNotMyComputer, - /** Should we ask the user if this is their computer, based on the last + /** + * Should we ask the user if this is their computer, based on the last * time they used browserid and the last time they answered a question * about this device * @param {integer} userid - the user's numeric id, returned * from session_context when authed. * @method usersComputer.seen */ shouldAsk: shouldAskUserAboutHerComputer, - /** Save the fact that a user has been seen on this computer before, but do not overwrite + /** + * Save the fact that a user has been seen on this computer before, but do not overwrite * existing state * @param {integer} userid - the user's numeric id, returned from session_context when authed. * @method usersComputer.setSeen */ - setSeen: setUserSeenOnComputer + setSeen: setUserSeenOnComputer, + /** + * Clear the status for the user + * @param {integer} userid - the user's numeric id, returned from session_context when authed. + * @method usersComputer.clear */ + clear: clearUsersComputerOwnershipStatus }, /** add email addresses to the email addy to userid mapping used when we're trying to determine diff --git a/resources/static/shared/user.js b/resources/static/shared/user.js index 5314a4fbecf18642d032d779224adcd4afde0084..5dfdf673db4538343f20eb4fe41aaf178f658da6 100644 --- a/resources/static/shared/user.js +++ b/resources/static/shared/user.js @@ -1083,11 +1083,10 @@ BrowserID.User = (function() { User.checkAuthentication(function(authenticated) { if (authenticated) { storage.setLoggedIn(origin, false); - if (onComplete) { - onComplete(true); - } - } else if (onComplete) { - onComplete(false); + } + + if (onComplete) { + onComplete(!!authenticated); } }, onFailure); }, diff --git a/resources/static/test/cases/controllers/is_this_your_computer.js b/resources/static/test/cases/controllers/is_this_your_computer.js new file mode 100644 index 0000000000000000000000000000000000000000..8e6702c9bb7e4bb29eeadb1dc3119434460a054c --- /dev/null +++ b/resources/static/test/cases/controllers/is_this_your_computer.js @@ -0,0 +1,50 @@ +/*jshint browsers:true, forin: true, laxbreak: true */ +/*global test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +(function() { + "use strict"; + + var controller, + el = $("body"), + bid = BrowserID, + user = bid.User, + xhr = bid.Mocks.xhr, + modules = bid.Modules, + testHelpers = bid.TestHelpers, + register = testHelpers.register; + + + module("controllers/is_this_your_computer", { + setup: function() { + testHelpers.setup(); + }, + + teardown: function() { + if (controller) { + try { + controller.destroy(); + controller = null; + } catch(e) { + // could already be destroyed from the close + } + } + testHelpers.teardown(); + } + }); + + function createController(options) { + controller = modules.IsThisYourComputer.create(); + controller.start(options || {}); + } + + test("yes - sets ownership flag to true for the user", function() { + console.log("add a test"); + }); + + test("no - set the ownership flag to false for the user", function() { + console.log("add a test"); + }); +}()); + diff --git a/resources/static/test/cases/resources/state.js b/resources/static/test/cases/resources/state.js index a0c7575a9d8b4a306e82ffb24f8decaa2a84fd3a..662de2576959644cacdeba517500c63621620c68 100644 --- a/resources/static/test/cases/resources/state.js +++ b/resources/static/test/cases/resources/state.js @@ -12,6 +12,7 @@ user = bid.User, machine, actions, + network = bid.Network, storage = bid.Storage, testHelpers = bid.TestHelpers, xhr = bid.Mocks.xhr; @@ -38,6 +39,19 @@ machine.start({controller: actions}); } + function setContextInfo(auth_status) { + // Make sure there is context info for network. + var serverTime = (new Date().getTime()) - 10; + mediator.publish("context_info", { + server_time: serverTime, + domain_key_creation_time: serverTime, + code_version: "ABCDEF", + auth_status: auth_status || "password", + userid: 1, + random_seed: "ABCDEFGH" + }); + } + module("resources/state", { setup: function() { testHelpers.setup(); @@ -51,10 +65,6 @@ }); - test("can create and start the machine", function() { - ok(machine, "Machine has been created"); - }); - test("attempt to create a state machine without a controller", function() { var error; try { @@ -158,13 +168,19 @@ ok(actions.called.doProvisionPrimaryUser, "doProvisionPrimaryUser called"); }); - test("primary_user_ready - call doEmailChosen", function() { + asyncTest("primary_user_ready - redirect to `email_chosen`", function() { + storage.addEmail("testuser@testuser.com", {}); + mediator.subscribe("email_chosen", function(msg, info) { + equal(info.email, "testuser@testuser.com", "correct email passed"); + start(); + }); + mediator.publish("primary_user_ready", { email: "testuser@testuser.com", assertion: "assertion" }); - ok(actions.called.doEmailChosen, "doEmailChosen called"); }); - asyncTest("authenticated - defer to `email_chosen`", function() { + asyncTest("authenticated - redirect to `email_chosen`", function() { + storage.addEmail("testuser@testuser.com", {}); mediator.subscribe("email_chosen", function(msg, data) { equal(data.email, "testuser@testuser.com"); start(); @@ -201,27 +217,37 @@ equal(actions.info.doAuthenticate.email, "testuser@testuser.com", "authenticate called with the correct email"); }); - test("assertion_generated with null assertion", function() { + asyncTest("assertion_generated with null assertion - redirect to pick_email", function() { + mediator.subscribe("pick_email", function() { + ok(true, "redirect to pick_email"); + start(); + }); mediator.publish("assertion_generated", { assertion: null }); - - equal(actions.called.doPickEmail, true, "now picking email because of null assertion"); }); - test("assertion_generated with assertion", function() { + asyncTest("assertion_generated with assertion, need to ask user whether it's their computer - redirect to is_this_your_computer", function() { + setContextInfo("password"); + // clear the user's seen info which causes the question to be asked. + storage.usersComputer.clear(network.userid()); + mediator.subscribe("is_this_your_computer", function() { + ok(true, "redirect to is_this_your_computer"); + start(); + }); + mediator.publish("assertion_generated", { assertion: "assertion" }); - - equal(actions.info.doAssertionGenerated, "assertion", "assertion generated with good assertion"); }); - test("add_email - call doAddEmail", function() { - mediator.publish("add_email", { email: "testuser@testuser.com" }); + test("assertion_generated with assertion, do not ask user whether it's their computer - doAssertionGenerated called", function() { + setContextInfo("password"); + mediator.publish("assertion_generated", { + assertion: "assertion" + }); - ok(actions.called.doAddEmail, "user wants to add an email"); - ok(actions.info.doAddEmail.email, "testuser@testuser.com", "correct email passed"); + equal(actions.info.doAssertionGenerated, "assertion", "assertion generated with good assertion"); }); test("email_confirmed", function() { diff --git a/resources/static/test/testHelpers/helpers.js b/resources/static/test/testHelpers/helpers.js index 493ea1db0756779b69735a7f18c104b51c1d3e26..31ac2d086649287fd4b4d68810fed152a9cdeab7 100644 --- a/resources/static/test/testHelpers/helpers.js +++ b/resources/static/test/testHelpers/helpers.js @@ -47,6 +47,12 @@ BrowserID.TestHelpers = (function() { ok($("#error #network").text().length, "network contents have been written"); } + function clearStorage() { + for(var key in localStorage) { + localStorage.removeItem(key); + } + } + var TestHelpers = { XHR_TIME_UNTIL_DELAY: 100, setup: function() { @@ -63,7 +69,7 @@ BrowserID.TestHelpers = (function() { transport.useResult("valid"); network.init(); - storage.clear(); + clearStorage(); $("body").stop().show(); $("body")[0].className = ""; @@ -93,7 +99,7 @@ BrowserID.TestHelpers = (function() { time_until_delay: 10 * 1000 }); network.init(); - storage.clear(); + clearStorage(); screens.wait.hide(); screens.error.hide(); screens.delay.hide(); diff --git a/resources/views/test.ejs b/resources/views/test.ejs index 036d577407b9b3fa9cb039104b09bfd1b30bdc67..f8af4e276a00038e712becbc3c9fe2ec5f49a26c 100644 --- a/resources/views/test.ejs +++ b/resources/views/test.ejs @@ -124,6 +124,7 @@ <script src="/dialog/controllers/email_chosen.js"></script> <script src="/dialog/controllers/provision_primary_user.js"></script> <script src="/dialog/controllers/primary_user_provisioned.js"></script> + <script src="/dialog/controllers/is_this_your_computer.js"></script> <script src="/pages/page_helpers.js"></script> <script src="/pages/add_email_address.js"></script> @@ -181,6 +182,7 @@ <script src="cases/controllers/email_chosen.js"></script> <script src="cases/controllers/provision_primary_user.js"></script> <script src="cases/controllers/primary_user_provisioned.js"></script> + <script src="cases/controllers/is_this_your_computer.js"></script> <!-- must go last or all other tests will fail. --> <script src="cases/controllers/dialog.js"></script>