diff --git a/.gitignore b/.gitignore index 1823f2e57..b0346bd65 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ .\#* /node_modules /var +/rpmbuild + diff --git a/ChangeLog b/ChangeLog index 1fae26146..4afca93de 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,16 @@ +train-2011.10.27: + * link fixing ('need help?' to point to SUMO): #378 + * unit tests repaired: #469 (broken in fix to #82) + * improve handling of network errors: #448 + * improve styling and language of email confirmation page: #349 + * logging improvements: #455 + * RPM generation script created (for installation of browserid on redhat [moz prod] boxes): #478 + * SCHEMA CHANGES to improve database performance and scalability: #480 + * change the health check call from '/ping.txt' to '/__heartbeat__': #481 + * remove application level network timeouts (let the network stack do its job, the user can cancel if they get sick of it): #485 + * improve messaging for unsupported browsers: #273, #484 + * developer documentation improvements: #496 + train-2011.10.20: * android < 3.0 now supported: #461 * properly set assertion expiration time to when they expire, not when they're issued: #433, #457, #458 diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 07fe7cb6e..1e8b7b606 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -156,16 +156,16 @@ post update hook, annotated to help you follow along: ### 5. get node servers running At this point, pushing code to gitolite will cause /home/browserid/code to be updated. Now -we need to get the servers running! Manually we can verify that the servers will run. +we need to get the servers running! Manually we can verify that the servers will run. For the browser id server: - cd /home/browserid/code/browserid && sudo -u www-data ./run.js + cd /home/browserid/code/browserid && sudo -u www-data ./run.js And for the verifier: - cd /home/browserid/code/verifier && sudo -u www-data ./run.js + cd /home/browserid/code/verifier && sudo -u www-data ./run.js -Now let's set up [monit] to restart the node.js servers: +Now let's set up [monit] to restart the node.js servers: 1. install monit: `sudo apt-get install monit` 2. enable monit by editing `/etc/default/monit` @@ -181,7 +181,7 @@ include /etc/monit.d/*
 #!/bin/bash
-/usr/local/bin/node $1 > $(dirname $1)/error.log 2>&1 &    
+/usr/local/bin/node $1 > $(dirname $1)/error.log 2>&1 &
 
5. create a file to run the verifier at `/etc/monit.d/verifier`: @@ -192,7 +192,7 @@ check host verifier with address 127.0.0.1 as uid "www-data" and gid "www-data" stop program = "/usr/bin/pkill -f '/usr/local/bin/node /home/browserid/code/verifier/run.js'" if failed port 62800 protocol HTTP - request /ping.txt + request /__heartbeat__ with timeout 10 seconds then restart @@ -205,7 +205,7 @@ check host browserid.org with address 127.0.0.1 as uid "www-data" and gid "www-data" stop program = "/usr/bin/pkill -f '/usr/local/bin/node /home/browserid/code/browserid/run.js'" if failed port 62700 protocol HTTP - request /ping.txt + request /__heartbeat__ with timeout 10 seconds then restart diff --git a/README.md b/README.md index 9ecb6efd1..908b6ea4a 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,12 @@ or changes will be made. 2. Boot up the VM: - $ cd browserid - $ vagrant up - $ vagrant ssh - vagrant@lucid32:browserid$ node ./run.js +``` +cd browserid +vagrant up +vagrant ssh vagrant@lucid32:browserid +node ./run.js +``` `vagrant up` will take a while. Go get a cup of coffee. This is because it downloads the 500MB VM. diff --git a/browserid.spec b/browserid.spec new file mode 100644 index 000000000..368194a57 --- /dev/null +++ b/browserid.spec @@ -0,0 +1,45 @@ +%define _rootdir /opt/browserid + +Name: browserid-server +Version: 0.2011.10.13 +Release: 1%{?dist} +Summary: BrowserID server +Packager: Pete Fritchman +Group: Development/Libraries +License: MPL 1.1+/GPL 2.0+/LGPL 2.1+ +URL: https://github.com/mozilla/browserid +Source0: %{name}.tar.gz +BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root +AutoReqProv: no +Requires: openssl nodejs +BuildRequires: gcc-c++ git jre make npm openssl-devel + +%description +browserid server & web home for browserid.org + +%prep +%setup -q -n browserid + +%build +npm install +export PATH=$PWD/node_modules/.bin:$PATH +(cd browserid && ./compress.sh) +git log -1 --oneline > browserid/static/ver.txt + +%install +rm -rf %{buildroot} +mkdir -p %{buildroot}%{_rootdir} +for f in browserid libs node_modules verifier *.json *.js; do + cp -rp $f %{buildroot}%{_rootdir}/$dir +done + +%clean +rm -rf %{buildroot} + +%files +%defattr(-,root,root,-) +%{_rootdir} + +%changelog +* Tue Oct 18 2011 Pete Fritchman +- Initial version diff --git a/browserid/app.js b/browserid/app.js index a0f26efc2..6f661ee7c 100644 --- a/browserid/app.js +++ b/browserid/app.js @@ -45,6 +45,7 @@ express = require('express'), secrets = require('../libs/secrets.js'), db = require('./lib/db.js'), configuration = require('../libs/configuration.js'), +heartbeat = require('../libs/heartbeat.js'), substitution = require('../libs/substitute.js'); metrics = require("../libs/metrics.js"), logger = require("../libs/logging.js").logger; @@ -79,11 +80,16 @@ function router(app) { metrics.userEntry(req); res.render('dialog.ejs', { title: 'A Better Way to Sign In', - layout: false, + layout: 'dialog_layout.ejs', + useJavascript: true, production: configuration.get('use_minified_resources') }); }); + app.get("/unsupported_dialog", function(req,res) { + res.render('unsupported_dialog.ejs', {layout: 'dialog_layout.ejs', useJavascript: false}); + }); + // simple redirects (internal for now) app.get('/register_iframe', internal_redirector('/dialog/register_iframe.html',true)); @@ -102,9 +108,6 @@ function router(app) { res.render('index.ejs', {title: 'A Better Way to Sign In', fullpage: true}); }); - // BA removed .html URLs. If we have 404s, - // we should set up some redirects - app.get("/signup", function(req, res) { res.render('signup.ejs', {title: 'Sign Up', fullpage: false}); }); @@ -141,7 +144,7 @@ function router(app) { REDIRECTS = { "/manage": "/", "/users": "/", - "/users/": "/", + "/users/": "/", "/primaries" : "/developers", "/primaries/" : "/developers", "/developers" : "https://github.com/mozilla/browserid/wiki/How-to-Use-BrowserID-on-Your-Site" @@ -160,6 +163,9 @@ function router(app) { // register all the WSAPI handlers wsapi.setup(app); + // setup health check / heartbeat + heartbeat.setup(app); + // the public key app.get("/pk", function(req, res) { res.json(ca.PUBLIC_KEY.toSimpleObject()); diff --git a/browserid/lib/db_mysql.js b/browserid/lib/db_mysql.js index 06bd219bf..9a7dbc838 100644 --- a/browserid/lib/db_mysql.js +++ b/browserid/lib/db_mysql.js @@ -49,6 +49,7 @@ * * * +------ staged ----------+ + * |*int id | * |*string secret | * | bool new_acct | * | string existing | @@ -67,10 +68,28 @@ var client = undefined; // may get defined at open() time causing a database to be dropped upon connection closing. var drop_on_close = undefined; +// If you change these schemas, please notify const schemas = [ - "CREATE TABLE IF NOT EXISTS user ( id INTEGER AUTO_INCREMENT PRIMARY KEY, passwd VARCHAR(64) );", - "CREATE TABLE IF NOT EXISTS email ( id INTEGER AUTO_INCREMENT PRIMARY KEY, user INTEGER, INDEX(user), address VARCHAR(255) UNIQUE, INDEX(address) );", - "CREATE TABLE IF NOT EXISTS staged ( secret VARCHAR(48) PRIMARY KEY, new_acct BOOL, existing VARCHAR(255), email VARCHAR(255) UNIQUE, INDEX(email), ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP);" + "CREATE TABLE IF NOT EXISTS user (" + + "id BIGINT AUTO_INCREMENT PRIMARY KEY," + + "passwd CHAR(64) NOT NULL" + + ") ENGINE=InnoDB;", + + "CREATE TABLE IF NOT EXISTS email (" + + "id BIGINT AUTO_INCREMENT PRIMARY KEY," + + "user BIGINT NOT NULL," + + "address VARCHAR(255) UNIQUE NOT NULL," + + "FOREIGN KEY user_fkey (user) REFERENCES user(id)" + + ") ENGINE=InnoDB;", + + "CREATE TABLE IF NOT EXISTS staged (" + + "id BIGINT AUTO_INCREMENT PRIMARY KEY," + + "secret CHAR(48) UNIQUE NOT NULL," + + "new_acct BOOL NOT NULL," + + "existing VARCHAR(255)," + + "email VARCHAR(255) UNIQUE NOT NULL," + + "ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL" + + ") ENGINE=InnoDB;", ]; // log an unexpected database error @@ -111,37 +130,39 @@ exports.open = function(cfg, cb) { } // now create the databse - client.query("CREATE DATABASE IF NOT EXISTS " + database, function(err) { - if (err) { - logUnexpectedError(err); - cb(err); - return; - } - client.useDatabase(database, function(err) { + if (cfg.create_schema || cfg.unit_test) { + client.query("CREATE DATABASE IF NOT EXISTS " + database, function(err) { if (err) { logUnexpectedError(err); cb(err); return; } + client.useDatabase(database, function(err) { + if (err) { + logUnexpectedError(err); + cb(err); + return; + } - // now create tables - function createNextTable(i) { - if (i < schemas.length) { - client.query(schemas[i], function(err) { - if (err) { - logUnexpectedError(err); - cb(err); - } else { - createNextTable(i+1); - } - }); - } else { - cb(); + // now create tables + function createNextTable(i) { + if (i < schemas.length) { + client.query(schemas[i], function(err) { + if (err) { + logUnexpectedError(err); + cb(err); + } else { + createNextTable(i+1); + } + }); + } else { + cb(); + } } - } - createNextTable(0); + createNextTable(0); + }); }); - }); + }; }; exports.close = function(cb) { diff --git a/browserid/lib/wsapi.js b/browserid/lib/wsapi.js index 9cd1bf8b0..9c75fdbfc 100644 --- a/browserid/lib/wsapi.js +++ b/browserid/lib/wsapi.js @@ -278,7 +278,7 @@ function setup(app) { db.gotVerificationSecret(req.body.token, hash, function(err, email) { if (err) { - logger.error("error completing the verification: " + err); + logger.warn("couldn't complete email verification: " + err); resp.json({ success: false }); } else { // FIXME: not sure if we want to do this (ba) @@ -360,7 +360,7 @@ function setup(app) { app.post('/wsapi/complete_email_addition', checkParams(["token"]), function(req, resp) { db.gotVerificationSecret(req.body.token, undefined, function(e) { if (e) { - logger.error("error completing the verification: " + e); + logger.warn("couldn't complete email verification: " + e); resp.json({ success: false }); } else { resp.json({ success: true }); diff --git a/browserid/static/css/style.css b/browserid/static/css/style.css index 9023ebbcb..20c2e610d 100644 --- a/browserid/static/css/style.css +++ b/browserid/static/css/style.css @@ -21,30 +21,24 @@ body { overflow-y: scroll; } -/*header { - border-top: 4px solid #333; - background-color: #333; - padding: 0 20% 0 20%; - margin: 0; - background: #008; - height: 230px; - color #fff; - min-width: 800px; - display: block; +/* for floats */ +.cf:after { + content: "."; + display: block; + clear: both; + visibility: hidden; + line-height: 0; + height: 0; } -footer { - background-color: #F1F1F1; - border-top: 2px solid #ddd; - margin: 0; - margin-top: 100px; - padding: 0; - height: 200px; - font-size: 1.1em; - display: block; +html[xmlns] .cf { + display: block; +} + +* html .cf { + height: 1%; } -*/ .sans { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } @@ -115,11 +109,6 @@ hr { display: none; } -/* -.authenticated #content { - display: block; -} -*/ #about { font-family: 'Droid Serif', Georgia, serif; font-size: 14px; @@ -623,7 +612,7 @@ h1 { display: inline-block; } -#signUpForm { +#signUpForm, #congrats { padding: 20px; background-color: rgba(0,0,0,0.035); @@ -656,33 +645,6 @@ h1 { float: left; } -a.forgot { - color: #888784; - text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.5); - font-size: 11px; -} - -.notifications { - list-style-type: none; -} - -.notifications > .notification { - margin-top: 20px; - padding: 5px; - line-height: 16px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - -o-border-radius: 3px; - border-radius: 3px; - display: none; - text-align: center; -} - -.notifications .notification.error { - color: red; - background-color: rgba(255,0,0,0.25); -} - #signUpForm .red { color: red; } @@ -748,6 +710,64 @@ a.forgot { float: right; } +#signUpForm .error { + margin-top: 20px; + color: red; + background-color: rgba(255,0,0,0.25); + padding: 5px; + line-height: 16px; + + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + -o-border-radius: 3px; + border-radius: 3px; +} + + +#congrats #siteinfo, #congrats { + display: none; +} + +#congrats p { + color: #62615F; + display: block; + text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.5); +} + +#congrats .website { + display: block; + text-align: center; +} + + +a.forgot { + color: #888784; + text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.5); + font-size: 11px; +} + +.notifications { + list-style-type: none; +} + +.notifications > .notification { + margin-top: 20px; + padding: 5px; + line-height: 16px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + -o-border-radius: 3px; + border-radius: 3px; + display: none; + text-align: center; +} + +.notifications .notification.error { + color: red; + background-color: rgba(255,0,0,0.25); +} + + #header { position: absolute; top: 0; @@ -830,172 +850,3 @@ a.forgot { color: #aaa; } - -/* for floats */ -.cf:after { - content: "."; - display: block; - clear: both; - visibility: hidden; - line-height: 0; - height: 0; -} - -html[xmlns] .cf { - display: block; -} - -* html .cf { - height: 1%; -} - -#signUpFormWrap { - margin: 122px 160px; -} - -#signUpFormWrap a.signUpIn { - color: #549FDC; - text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.5); -} - -#signUpFormWrap a.space { - margin: 10px 20px 0; - display: inline-block; -} - -#signUpForm, -#congrats { - padding: 20px; - background-color: rgba(0,0,0,0.035); - - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - -o-border-radius: 5px; - border-radius: 5px; -} - -#congrats { - display: none; -} - -#congrats p { - color: #62615F; - display: block; - text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.5); -} - -#signUpForm label/*, -#signUpForm p.hint*/ { - display: block; - color: #62615F; - text-shadow: 1px 1px 0 rgba(255,255,255,0.5); -} - -/* - -#signUpForm p.hint { - margin: -20px -20px 20px; - padding: 10px 20px 10px 45px; - background-color: rgba(0,0,0,0.05); - background-repeat: no-repeat; - background-position: 20px center; - color: #222; - - -webkit-border-radius: 5px 5px 0 0; - -moz-border-radius: 5px 5px 0 0; - -o-border-radius: 5px 5px 0 0; - border-radius: 5px 5px 0 0; -} -*/ -#signUpForm label.half, -.half { - width: 50%; - display: inline-block; - float: left; -} - -a.forgot { - color: #888784; - text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.5); - font-size: 11px; -} - -#signUpForm .error { - margin-top: 20px; - color: red; - background-color: rgba(255,0,0,0.25); - padding: 5px; - line-height: 16px; - - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - -o-border-radius: 3px; - border-radius: 3px; -} - -#signUpForm .red { - color: red; -} - -#signUpForm input[type=email], -#signUpForm input[type=password] { - width: 100%; - font-size: 14px; - padding: 5px; - border-width: 1px; - border-style: solid; - border-color: #A3A29D #C6C3B4 #C6C3B4 #A3A29D; - outline: none; - - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - -o-border-radius: 3px; - border-radius: 3px; - - -webkit-box-shadow: 1px 1px 0 rgba(255,255,255,0.5); - -moz-box-shadow: 1px 1px 0 rgba(255,255,255,0.5); - -o-box-shadow: 1px 1px 0 rgba(255,255,255,0.5); - box-shadow: 1px 1px 0 rgba(255,255,255,0.5); -} - -#signUpForm input[type=email]:focus, -#signUpForm input[type=password]:focus { - border: 1px solid #549FDC; - - -webkit-border-radius: 0; - -moz-border-radius: 0; - -o-border-radius: 0; - border-radius: 0; - - -webkit-box-shadow: 0 0 0 1px #549FDC inset; - -moz-box-shadow: 0 0 0 1px #549FDC inset; - -o-box-shadow: 0 0 0 1px #549FDC inset; - box-shadow: 0 0 0 1px #549FDC inset; -} - -#signUpForm .submit { - height: 28px; -} - -#signUpForm .remember { - display: inline-block; - line-height: 28px; -} - -#signUpForm .remember .checkAlign { - float: left; -} - -#signUpForm .remember label { - margin-left: 5px; - float: left; -} - -#signUpForm .create { - font-size: 14px; - height: 28px; - padding: 0 10px; - float: right; -} - - diff --git a/browserid/static/dialog/controllers/authenticate_controller.js b/browserid/static/dialog/controllers/authenticate_controller.js index 0054f37c3..84632f5f3 100644 --- a/browserid/static/dialog/controllers/authenticate_controller.js +++ b/browserid/static/dialog/controllers/authenticate_controller.js @@ -40,6 +40,7 @@ var ANIMATION_TIME = 250, bid = BrowserID, user = bid.User, + errors = bid.Errors, validation = bid.Validation, lastEmail = ""; @@ -60,7 +61,7 @@ else { createUserState.call(self); } - }); + }, self.getErrorDialog(errors.isEmailRegistered)); } function createUser(el, event) { @@ -83,7 +84,7 @@ else { // XXX can't register this email address. } - }, self.getErrorDialog(bid.Errors.createAccount)); + }, self.getErrorDialog(errors.createUser)); } function authenticate(el, event) { @@ -106,10 +107,7 @@ } else { bid.Tooltip.showTooltip("#cannot_authenticate"); } - }, - self.getErrorDialog(bid.Errors.authentication) - ); - + }, self.getErrorDialog(errors.authenticate)); } function resetPassword(el, event) { @@ -122,9 +120,7 @@ self.close("reset_password", { email: email }); - }, function() { - // XXX TODO error screen! - }); + }, self.getErrorDialog(errors.requestPasswordReset)); } function animateSwap(fadeOutSelector, fadeInSelector, callback) { diff --git a/browserid/static/dialog/controllers/dialog_controller.js b/browserid/static/dialog/controllers/dialog_controller.js index 491d983fa..49560e08d 100644 --- a/browserid/static/dialog/controllers/dialog_controller.js +++ b/browserid/static/dialog/controllers/dialog_controller.js @@ -42,33 +42,40 @@ (function() { "use strict"; - var user = BrowserID.User; + var bid = BrowserID, + user = bid.User, + errors = bid.Errors, + offline = false; PageController.extend("Dialog", {}, { init: function(el) { var self=this; - //this.element.show(); // keep track of where we are and what we do on success and error self.onsuccess = null; self.onerror = null; setupChannel(self); self.stateMachine(); + }, getVerifiedEmail: function(origin_url, onsuccess, onerror) { - this.onsuccess = onsuccess; - this.onerror = onerror; + var self=this; + self.onsuccess = onsuccess; + self.onerror = onerror; + + if('onLine' in navigator && !navigator.onLine) { + self.doOffline(); + return; + } user.setOrigin(origin_url); - - // get the cleaned origin. $("#sitename").text(user.getHostname()); - this.doCheckAuth(); + self.doCheckAuth(); - var self=this; $(window).bind("unload", function() { + bid.Storage.setStagedOnBehalfOf(""); self.doCancel(); }); }, @@ -78,7 +85,15 @@ var self=this, hub = OpenAjax.hub, el = this.element; - + + hub.subscribe("offline", function(msg, info) { + self.doOffline(); + }); + + hub.subscribe("xhrError", function(msg, info) { + //self.doXHRError(info); + // XXX how are we going to handle this? + }); hub.subscribe("user_staged", function(msg, info) { self.doConfirmUser(info.email); @@ -100,7 +115,7 @@ }); hub.subscribe("assertion_generated", function(msg, info) { - if(info.assertion !== null) { + if (info.assertion !== null) { self.doAssertionGenerated(info.assertion); } else { @@ -138,6 +153,15 @@ }, + doOffline: function() { + this.renderError("wait.ejs", errors.offline); + offline = true; + }, + + doXHRError: function(info) { + if (!offline) this.renderError("wait.ejs", errors.offline); + }, + doConfirmUser: function(email) { this.confirmEmail = email; @@ -150,7 +174,7 @@ doCancel: function() { var self=this; - if(self.onsuccess) { + if (self.onsuccess) { self.onsuccess(null); } }, @@ -182,7 +206,8 @@ doEmailConfirmed: function() { var self=this; // yay! now we need to produce an assertion. - user.getAssertion(this.confirmEmail, self.doAssertionGenerated.bind(self)); + user.getAssertion(this.confirmEmail, self.doAssertionGenerated.bind(self), + self.getErrorDialog(errors.getAssertion)); }, doAssertionGenerated: function(assertion) { @@ -195,16 +220,16 @@ }, doNotMe: function() { - user.logoutUser(this.doAuthenticate.bind(this)); + var self=this; + user.logoutUser(self.doAuthenticate.bind(self), self.getErrorDialog(errors.logoutUser)); }, syncEmails: function() { var self = this; user.syncEmails(self.doPickEmail.bind(self), - self.getErrorDialog(BrowserID.Errors.signIn)); + self.getErrorDialog(errors.signIn)); }, - doCheckAuth: function() { var self=this; user.checkAuthenticationAndSync(function onSuccess() {}, @@ -215,7 +240,7 @@ self.doAuthenticate(); } }, - self.getErrorDialog(BrowserID.Errors.checkAuthentication)); + self.getErrorDialog(errors.checkAuthentication)); } }); diff --git a/browserid/static/dialog/controllers/page_controller.js b/browserid/static/dialog/controllers/page_controller.js index f3031469a..01ddbb26a 100644 --- a/browserid/static/dialog/controllers/page_controller.js +++ b/browserid/static/dialog/controllers/page_controller.js @@ -48,6 +48,8 @@ var me=this, bodyTemplate = options.bodyTemplate, bodyVars = options.bodyVars, + errorTemplate = options.errorTemplate, + errorVars = options.errorVars, waitTemplate = options.waitTemplate, waitVars = options.waitVars; @@ -60,6 +62,10 @@ me.renderWait(waitTemplate, waitVars); } + if(errorTemplate) { + me.renderError(errorTemplate, errorVars); + } + // XXX move all of these, bleck. $("form").bind("submit", me.onSubmit.bind(me)); $("#cancel").click(me.onCancel.bind(me)); @@ -95,12 +101,12 @@ renderWait: function(body, body_vars) { this.renderTemplates("#wait", body, body_vars); - $("body").removeClass("error").removeClass("form").addClass("waiting"); - $("#wait").stop().css('opacity', 1).hide().fadeIn(ANIMATION_TIME); + $("body").removeClass("error").removeClass("form").addClass("waiting").css('opacity', 1); + $("#wait").stop().hide().fadeIn(ANIMATION_TIME); }, - renderError: function(error_vars) { - this.renderTemplates("#error", "wait.ejs", error_vars); + renderError: function(body, body_vars) { + this.renderTemplates("#error", body, body_vars); $("body").removeClass("waiting").removeClass("form").addClass("error"); $("#error").stop().css('opacity', 1).hide().fadeIn(ANIMATION_TIME); }, @@ -144,7 +150,7 @@ */ getErrorDialog: function(info) { var self=this; - return self.renderError.bind(self, info); + return self.renderError.bind(self, "wait.ejs", info); }, onCancel: function(event) { diff --git a/browserid/static/dialog/controllers/pickemail_controller.js b/browserid/static/dialog/controllers/pickemail_controller.js index 43e23513b..1b3679c13 100644 --- a/browserid/static/dialog/controllers/pickemail_controller.js +++ b/browserid/static/dialog/controllers/pickemail_controller.js @@ -40,6 +40,7 @@ var ANIMATION_TIME = 250, bid = BrowserID, user = bid.User, + errors = bid.Errors, body = $("body"), animationComplete = body.innerWidth() < 640, assertion; @@ -109,13 +110,13 @@ var self=this; user.getAssertion(email, function(assert) { assertion = assert || null; - tryClose.call(self); - }); + startAnimation.call(self); + }, self.getErrorDialog(errors.getAssertion)); } function startAnimation() { + var self=this; if(!animationComplete) { - var self=this; $("#signIn").animate({"width" : "685px"}, "slow", function () { // post animation body.delay(500).animate({ "opacity" : "0.5"}, "fast", function () { @@ -124,6 +125,9 @@ }); }); } + else { + tryClose.call(self); + } } @@ -134,9 +138,7 @@ var valid = checkEmail.call(self, email); if (valid) { -// self.doWait(bid.Wait.generateKey); getAssertion.call(self, email); - startAnimation.call(self); } } @@ -168,7 +170,7 @@ bid.Tooltip.showTooltip("#could_not_add"); }); } - }); + }, self.getErrorDialog(errors.isEmailRegistered)); } diff --git a/browserid/static/dialog/css/m.css b/browserid/static/dialog/css/m.css index 49f4a5613..9ddb605ea 100644 --- a/browserid/static/dialog/css/m.css +++ b/browserid/static/dialog/css/m.css @@ -111,6 +111,19 @@ height: 250px; } -} + #error .vertical { + width: auto; + } + #error .vertical > div { + display: block; + height: auto; + padding: 10px; + } + + #error #borderbox { + border-left: none; + padding: 0; + } + diff --git a/browserid/static/dialog/css/popup.css b/browserid/static/dialog/css/popup.css index b1b54243f..441efacb3 100644 --- a/browserid/static/dialog/css/popup.css +++ b/browserid/static/dialog/css/popup.css @@ -18,6 +18,7 @@ body { font-size: 13px; line-height: 21px; background-image: url('/i/bg.png'); + color: #000; } @@ -118,16 +119,17 @@ section > .contents { #wait, #error { text-align: center; - background-image: url("/i/bg.png"); } #wait { z-index: 1; + background-image: url("/i/bg.png"); } #error { display: none; z-index: 2; + background-color: #fff; } #wait strong, #error strong { @@ -135,10 +137,40 @@ section > .contents { font-weight: bold; } -#error { - z-index: 2; + +#error .vertical { + width: 630px; + margin: 0 auto; + display: block; +} + + +#error .vertical > div { + display: table-cell; + vertical-align: middle; + padding: 0 10px; + height: 250px; +} + +#error #alternative a { + color: #549FDC; + text-decoration: underline; +} + +#error #borderbox { + border-left: 1px solid #777; + padding: 20px 0; } +#error #borderbox img { + border: none; +} + +#error #alternative .lighter { + color: #777; +} + + #formWrap { background-color: #fff; background-image: none; diff --git a/browserid/static/dialog/qunit.html b/browserid/static/dialog/qunit.html index 108cc60fd..7e8189f82 100644 --- a/browserid/static/dialog/qunit.html +++ b/browserid/static/dialog/qunit.html @@ -12,23 +12,31 @@

-
+
+
    +
    -
    -
    -
    +

    Content below here is test content that can be ignored

    -
    -
    -
    +
    -
    -
    -
    +
    +
    +
    +
    +
    + +
    +
    +
    + + + Cannot confirm + Cannot communicate + + Hint
    -
      -
      diff --git a/browserid/static/dialog/resources/browser-support.js b/browserid/static/dialog/resources/browser-support.js new file mode 100644 index 000000000..6c2e34818 --- /dev/null +++ b/browserid/static/dialog/resources/browser-support.js @@ -0,0 +1,118 @@ +/*globals 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.BrowserSupport = (function() { + var bid = BrowserID, + win = window, + nav = navigator, + reason; + + // For unit testing + function setTestEnv(newNav, newWindow) { + nav = newNav; + win = newWindow; + } + + function getInternetExplorerVersion() { + var rv = -1; // Return value assumes failure. + if (nav.appName == 'Microsoft Internet Explorer') { + var ua = nav.userAgent; + var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); + if (re.exec(ua) != null) + rv = parseFloat(RegExp.$1); + } + + return rv; + } + + function checkIE() { + var ieVersion = getInternetExplorerVersion(), + ieNosupport = ieVersion > -1 && ieVersion < 9; + + if(ieNosupport) { + return "IE_VERSION"; + } + } + + function explicitNosupport() { + return checkIE(); + } + + function checkLocalStorage() { + var localStorage = 'localStorage' in win && win['localStorage'] !== null; + if(!localStorage) { + return "LOCALSTORAGE"; + } + } + + function checkPostMessage() { + if(!win.postMessage) { + return "POSTMESSAGE"; + } + } + + function isSupported() { + reason = checkLocalStorage() || checkPostMessage() || explicitNosupport(); + + return !reason; + } + + function getNoSupportReason() { + return reason; + } + + return { + /** + * Set the test environment. + * @method setTestEnv + */ + setTestEnv: setTestEnv, + /** + * Check whether the current browser is supported + * @method isSupported + * @returns {boolean} + */ + isSupported: isSupported, + /** + * Called after isSupported, if isSupported returns false. Gets the reason + * why browser is not supported. + * @method getNoSupportReason + * @returns {string} + */ + getNoSupportReason: getNoSupportReason + }; + +}()); + diff --git a/browserid/static/dialog/resources/error-messages.js b/browserid/static/dialog/resources/error-messages.js index 860760fb8..7064d82d9 100644 --- a/browserid/static/dialog/resources/error-messages.js +++ b/browserid/static/dialog/resources/error-messages.js @@ -36,7 +36,7 @@ BrowserID.Errors = (function(){ "use strict"; var Errors = { - authentication: { + authenticate: { type: "serverError", title: "Error Authenticating", message: "There was a technical problem while trying to log you in. Yucky!" @@ -54,18 +54,48 @@ BrowserID.Errors = (function(){ message: "There was a technical problem while trying to log you in. Yucky!" }, - createAccount: { + createUser: { type: "serverError", title: "Error Creating Account", message: "There was a technical problem while trying to create your account. Yucky!" }, + getAssertion: { + type: "serverError", + title: "Error Getting Assertion", + message: "There was a technical problem while trying to authenticate you. Yucky!" + }, + + isEmailRegistered: { + type: "serverError", + title: "Error Checking Email Address", + message: "There was a technical problem while trying to check that email address. Yucky!" + }, + + logoutUser: { + type: "serverError", + title: "Logout Failed", + message: "An error was encountered while signing you out. Yucky!" + }, + + offline: { + type: "networkError", + title: "You are offline!", + message: "Unfortunately, BrowserID cannot communicate while offline!" + }, + registration: { type: "serverError", title: "Registration Failed", message: "An error was encountered and the signup cannot be completed. Yucky!" }, + requestPasswordReset: { + type: "serverError", + title: "Error Resetting Password", + message: "There was a technical problem while trying to reset your password." + }, + signIn: { type: "serverError", title: "Signin Failed", diff --git a/browserid/static/dialog/resources/network.js b/browserid/static/dialog/resources/network.js index 18d041256..6f012b6e6 100644 --- a/browserid/static/dialog/resources/network.js +++ b/browserid/static/dialog/resources/network.js @@ -37,37 +37,13 @@ BrowserID.Network = (function() { "use strict"; - var csrf_token; - var xhr = $; - var server_time; - var auth_status; + var csrf_token, + xhr = $, + server_time, + auth_status, + hub = window.OpenAjax && OpenAjax.hub; - function withContext(cb) { - if (typeof auth_status === 'boolean' && typeof csrf_token !== 'undefined') cb(); - else { - xhr.ajax({ - url: "/wsapi/session_context", - type: "GET", - success: function(result) { - csrf_token = result.csrf_token; - server_time = { - remote: result.server_time, - local: (new Date()).getTime() - }; - auth_status = result.authenticated; - _.defer(cb); - }, - dataType: "json" - }); - } - } - - function clearContext() { - var undef; - csrf_token = server_time = auth_status = undef; - } - - function createDeferred(cb) { + function deferResponse(cb) { if (cb) { return function() { var args = _.toArray(arguments); @@ -78,6 +54,26 @@ BrowserID.Network = (function() { } } + function xhrError(cb, errorMessage) { + return function() { + if (cb) cb(); + hub && hub.publish("xhrError", errorMessage); + }; + } + + function get(options) { + xhr.ajax({ + type: "GET", + url: options.url, + // We defer the responses because otherwise jQuery eats any exceptions + // that are thrown in the response handlers and it becomes very difficult + // to debug. + success: deferResponse(options.success), + error: deferResponse(xhrError(options.error, options.errorMessage)), + dataType: "json" + }); + } + function post(options) { withContext(function() { var data = options.data || {}; @@ -90,12 +86,44 @@ BrowserID.Network = (function() { type: "POST", url: options.url, data: data, - success: options.success, - error: options.error + // We defer the responses because otherwise jQuery eats any exceptions + // that are thrown in the response handlers and it becomes very difficult + // to debug. + success: deferResponse(options.success), + error: deferResponse(xhrError(options.error, options.errorMessage)) }); - }); + }, options.error); } + function withContext(cb, onFailure) { + if (typeof auth_status === 'boolean' && typeof csrf_token !== 'undefined') cb(); + else { + xhr.ajax({ + url: "/wsapi/session_context", + success: function(result) { + csrf_token = result.csrf_token; + server_time = { + remote: result.server_time, + local: (new Date()).getTime() + }; + auth_status = result.authenticated; + cb(); + }, + error: deferResponse(xhrError(onFailure)) + }); + } + } + + function clearContext() { + var undef; + csrf_token = server_time = auth_status = undef; + } + + // Not really part of the Network API, but related to networking + $(document).bind("offline", function() { + hub.publish("offline"); + }); + var Network = { /** * Set the XHR object and clear all context info. Used for testing. @@ -133,7 +161,7 @@ BrowserID.Network = (function() { // session, let's set it to perhaps save a network request // (to fetch session context). auth_status = authenticated; - _.delay(onSuccess, 0, authenticated); + if(onSuccess) onSuccess(authenticated); } catch (e) { onFailure("unexpected server response: " + e); } @@ -154,19 +182,20 @@ BrowserID.Network = (function() { withContext(function() { try { if (typeof auth_status !== 'boolean') throw "can't get authentication status!"; - _.delay(onSuccess, 0, auth_status); + if (onSuccess) onSuccess(auth_status); } catch(e) { if (onFailure) onFailure(e.toString()); } - }); + }, onFailure); }, /** * Log the authenticated user out * @method logout * @param {function} [onSuccess] - called on completion + * @param {function} [onFailure] - Called on XHR failure. */ - logout: function(onSuccess) { + logout: function(onSuccess, onFailure) { post({ url: "/wsapi/logout", success: function() { @@ -176,8 +205,9 @@ BrowserID.Network = (function() { // FIXME: we should return a confirmation that the // user was successfully logged out. auth_status = false; - if (onSuccess) _.defer(onSuccess); - } + if (onSuccess) onSuccess(); + }, + error: onFailure }); }, @@ -197,10 +227,7 @@ BrowserID.Network = (function() { site : origin }, success: function(status) { - var staged = status.success; - // why a delay here? Because of the test harness? - // shouldn't the delay be in the test harness? - _.delay(onSuccess, 0, staged); + if (onSuccess) onSuccess(status.success); }, error: onFailure }); @@ -215,10 +242,10 @@ BrowserID.Network = (function() { * I think so (BA). */ emailForVerificationToken: function(token, onSuccess, onFailure) { - xhr.ajax({ + get({ url : "/wsapi/email_for_token?token=" + encodeURIComponent(token), success: function(data) { - onSuccess(data.email); + if (onSuccess) onSuccess(data.email); }, error: onFailure }); @@ -231,12 +258,10 @@ BrowserID.Network = (function() { * @param {function} [onFailure] - Called on XHR failure. */ checkUserRegistration: function(email, onSuccess, onFailure) { - xhr.ajax({ + get({ url: "/wsapi/user_creation_status?email=" + encodeURIComponent(email), success: function(status, textStatus, jqXHR) { - if (onSuccess) { - _.delay(onSuccess, 0, status.status); - } + if (onSuccess) onSuccess(status.status); }, error: onFailure }); @@ -258,9 +283,28 @@ BrowserID.Network = (function() { pass: password }, success: function(status, textStatus, jqXHR) { - if (onSuccess) { - _.delay(onSuccess, 0, status.success); - } + if (onSuccess) onSuccess(status.success); + }, + error: onFailure + }); + }, + + /** + * Call with a token to prove an email address ownership. + * @method completeEmailRegistration + * @param {string} token - token proving email ownership. + * @param {function} [onSuccess] - Callback to call when complete. Called + * with one boolean parameter that specifies the validity of the token. + * @param {function} [onFailure] - Called on XHR failure. + */ + completeEmailRegistration: function(token, onSuccess, onFailure) { + post({ + url: "/wsapi/complete_email_addition", + data: { + token: token + }, + success: function(status, textStatus, jqXHR) { + if (onSuccess) onSuccess(status.success); }, error: onFailure }); @@ -275,7 +319,7 @@ BrowserID.Network = (function() { */ requestPasswordReset: function(email, origin, onSuccess, onFailure) { if (email) { - this.createUser(email, origin, onSuccess, onFailure); + Network.createUser(email, origin, onSuccess, onFailure); } else { // TODO: if no email is provided, then what? throw "no email provided to password reset"; @@ -291,9 +335,7 @@ BrowserID.Network = (function() { */ resetPassword: function(password, onSuccess, onFailure) { // XXX fill this in. - if (onSuccess) { - _.defer(onSuccess); - } + if (onSuccess) onSuccess(); }, /** @@ -308,32 +350,10 @@ BrowserID.Network = (function() { changePassword: function(oldPassword, newPassword, onSuccess, onFailure) { // XXX fill this in if (onSuccess) { - _.delay(onSuccess, 0, true); + onSuccess(true); } }, - /** - * Call with a token to prove an email address ownership. - * @method completeEmailRegistration - * @param {string} token - token proving email ownership. - * @param {function} [onSuccess] - Callback to call when complete. Called - * with one boolean parameter that specifies the validity of the token. - * @param {function} [onFailure] - Called on XHR failure. - */ - completeEmailRegistration: function(token, onSuccess, onFailure) { - post({ - url: "/wsapi/complete_email_addition", - data: { - token: token - }, - success: function(status, textStatus, jqXHR) { - if (onSuccess) { - _.delay(onSuccess, 0, status.success); - } - }, - error: onFailure - }); - }, /** * Cancel the current user"s account. @@ -344,7 +364,7 @@ BrowserID.Network = (function() { cancelUser: function(onSuccess, onFailure) { post({ url: "/wsapi/account_cancel", - success: createDeferred(onSuccess), + success: onSuccess, error: onFailure }); }, @@ -365,8 +385,7 @@ BrowserID.Network = (function() { site: origin }, success: function(status) { - var staged = status.success; - _.delay(onSuccess, 0, staged); + if (onSuccess) onSuccess(status.success); }, error: onFailure }); @@ -380,12 +399,10 @@ BrowserID.Network = (function() { * @param {function} [onfailure] - called on xhr failure. */ checkEmailRegistration: function(email, onSuccess, onFailure) { - xhr.ajax({ + get({ url: "/wsapi/email_addition_status?email=" + encodeURIComponent(email), success: function(status, textStatus, jqXHR) { - if (onSuccess) { - _.delay(onSuccess, 0, status.status); - } + if (onSuccess) onSuccess(status.status); }, error: onFailure }); @@ -401,12 +418,10 @@ BrowserID.Network = (function() { * @param {function} [onFailure] - Called on XHR failure. */ emailRegistered: function(email, onSuccess, onFailure) { - xhr.ajax({ + get({ url: "/wsapi/have_email?email=" + encodeURIComponent(email), success: function(data, textStatus, xhr) { - if(onSuccess) { - _.delay(onSuccess, 0, data.email_known); - } + if(onSuccess) onSuccess(data.email_known); }, error: onFailure }); @@ -426,11 +441,9 @@ BrowserID.Network = (function() { email: email }, success: function(status, textStatus, jqXHR) { - if (onSuccess) { - _.delay(onSuccess, 0, status.success); - } + if (onSuccess) onSuccess(status.success); }, - failure: onFailure + error: onFailure }); }, @@ -438,15 +451,15 @@ BrowserID.Network = (function() { * Certify the public key for the email address. * @method certKey */ - certKey: function(email, pubkey, onSuccess, onError) { + certKey: function(email, pubkey, onSuccess, onFailure) { post({ url: "/wsapi/cert_key", data: { email: email, pubkey: pubkey.serialize() }, - success: createDeferred(onSuccess), - error: onError + success: onSuccess, + error: onFailure }); }, @@ -455,10 +468,9 @@ BrowserID.Network = (function() { * @method listEmails */ listEmails: function(onSuccess, onFailure) { - xhr.ajax({ - type: "GET", + get({ url: "/wsapi/list_emails", - success: createDeferred(onSuccess), + success: onSuccess, error: onFailure }); }, @@ -482,7 +494,7 @@ BrowserID.Network = (function() { } catch(e) { onFailure(e.toString()); } - }); + }, onFailure); } }; diff --git a/browserid/static/dialog/resources/user.js b/browserid/static/dialog/resources/user.js index 27105f7f4..3fde2a9a9 100644 --- a/browserid/static/dialog/resources/user.js +++ b/browserid/static/dialog/resources/user.js @@ -108,6 +108,10 @@ BrowserID.User = (function() { // 'mustAuth' - user must authenticate // 'noRegistration' - no registration is in progress if (status === "complete" || status === "mustAuth") { + // As soon as the registration comes back as complete, we should + // ensure that the stagedOnBehalfOf is cleared so there is no stale + // data. + storage.setStagedOnBehalfOf(""); if (onSuccess) { onSuccess(status); } @@ -118,7 +122,7 @@ BrowserID.User = (function() { else if (onFailure) { onFailure(status); } - }); + }, onFailure); }; poll(); @@ -230,7 +234,7 @@ BrowserID.User = (function() { var self=this; // remember this for later - storage.setStagedOnBehalfOf(origin); + storage.setStagedOnBehalfOf(self.getHostname()); network.createUser(email, origin, function(created) { if (onSuccess) { @@ -250,6 +254,35 @@ BrowserID.User = (function() { registrationPoll(network.checkUserRegistration, email, onSuccess, onFailure); }, + /** + * Verify a user + * @method verifyUser + * @param {string} token - token to verify. + * @param {string} password - password to set for account. + * @param {function} [onSuccess] - Called to give status updates. + * @param {function} [onFailure] - Called on error. + */ + verifyUser: function(token, password, onSuccess, onFailure) { + network.emailForVerificationToken(token, function (email) { + var invalidInfo = { valid: false }; + if (email) { + network.completeUserRegistration(token, password, function (valid) { + var info = valid ? { + valid: valid, + email: email, + origin: storage.getStagedOnBehalfOf() + } : invalidInfo; + + storage.setStagedOnBehalfOf(""); + + if (onSuccess) onSuccess(info); + }, onFailure); + } else if(onSuccess) { + onSuccess(invalidInfo); + } + }, onFailure); + }, + /** * Set the password of the current user. * @method setPassword @@ -277,7 +310,7 @@ BrowserID.User = (function() { * identity. * @method cancelUser * @param {function} [onSuccess] - Called whenever complete. - * @param {function} [onFailure] - called on failure. + * @param {function} [onFailure] - called on error. */ cancelUser: function(onSuccess, onFailure) { network.cancelUser(function() { @@ -285,7 +318,7 @@ BrowserID.User = (function() { if (onSuccess) { onSuccess(); } - }); + }, onFailure); }, @@ -293,7 +326,7 @@ BrowserID.User = (function() { * Log the current user out. * @method logoutUser * @param {function} [onSuccess] - Called whenever complete. - * @param {function} [onFailure] - called on failure. + * @param {function} [onFailure] - called on error. */ logoutUser: function(onSuccess, onFailure) { network.logout(function() { @@ -301,7 +334,7 @@ BrowserID.User = (function() { if (onSuccess) { onSuccess(); } - }); + }, onFailure); }, /** @@ -309,7 +342,7 @@ BrowserID.User = (function() { * be called. * @method syncEmails * @param {function} [onSuccess] - Called whenever complete. - * @param {function} [onFailure] - Called on failure. + * @param {function} [onFailure] - Called on error. */ syncEmails: function(onSuccess, onFailure) { cleanupIdentities(); @@ -356,7 +389,7 @@ BrowserID.User = (function() { } addNextEmail(); - }); + }, onFailure); }, /** @@ -365,7 +398,7 @@ BrowserID.User = (function() { * @param {function} [onSuccess] - Called when check is complete with one * boolean parameter, authenticated. authenticated will be true if user is * authenticated, false otw. - * @param {function} [onFailure] - Called on failure. + * @param {function} [onFailure] - Called on error. */ checkAuthentication: function(onSuccess, onFailure) { network.checkAuth(function(authenticated) { @@ -384,7 +417,7 @@ BrowserID.User = (function() { * but before sync starts. Useful for displaying status messages about the * sync taking a moment. * @param {function} [onComplete] - Called on sync completion. - * @param {function} [onFailure] - Called on failure. + * @param {function} [onFailure] - Called on error. */ checkAuthenticationAndSync: function(onSuccess, onComplete, onFailure) { var self=this; @@ -413,7 +446,7 @@ BrowserID.User = (function() { * @param {string} email - Email address to authenticate. * @param {string} password - Password. * @param {function} [onComplete] - Called on sync completion. - * @param {function} [onFailure] - Called on failure. + * @param {function} [onFailure] - Called on error. */ authenticate: function(email, password, onComplete, onFailure) { var self=this; @@ -452,6 +485,7 @@ BrowserID.User = (function() { var self = this; network.addEmail(email, origin, function(added) { if (added) { + storage.setStagedOnBehalfOf(self.getHostname()); // we no longer send the keypair, since we will certify it later. if (onSuccess) { onSuccess(added); @@ -471,12 +505,42 @@ BrowserID.User = (function() { registrationPoll(network.checkEmailRegistration, email, onSuccess, onFailure); }, + /** + * Verify a users email address given by the token + * @method verifyEmail + * @param {string} token + * @param {function} [onSuccess] - Called on success. + * Called with an object with valid, email, and origin if valid, called + * with only valid otw. + * @param {function} [onFailure] - Called on error. + */ + verifyEmail: function(token, onSuccess, onFailure) { + network.emailForVerificationToken(token, function (email) { + var invalidInfo = { valid: false }; + if (email) { + network.completeEmailRegistration(token, function (valid) { + var info = valid ? { + valid: valid, + email: email, + origin: storage.getStagedOnBehalfOf() + } : invalidInfo; + + storage.setStagedOnBehalfOf(""); + + if (onSuccess) onSuccess(info); + }, onFailure); + } else if(onSuccess) { + onSuccess(invalidInfo); + } + }, onFailure); + }, + /** * Remove an email address. * @method removeEmail * @param {string} email - Email address to remove. * @param {function} [onSuccess] - Called when complete. - * @param {function} [onFailure] - Called on failure. + * @param {function} [onFailure] - Called on error. */ removeEmail: function(email, onSuccess, onFailure) { if(storage.getEmail(email)) { @@ -513,7 +577,7 @@ BrowserID.User = (function() { * @method getAssertion * @param {string} email - Email to get assertion for. * @param {function} [onSuccess] - Called with assertion on success. - * @param {function} [onFailure] - Called on failure. + * @param {function} [onFailure] - Called on error. */ getAssertion: function(email, onSuccess, onFailure) { // we use the current time from the browserid servers @@ -533,7 +597,7 @@ BrowserID.User = (function() { if (onSuccess) { onSuccess(assertion); } - }); + }, onFailure); } if (storedID) { diff --git a/browserid/static/dialog/test/qunit/controllers/page_controller_unit_test.js b/browserid/static/dialog/test/qunit/controllers/page_controller_unit_test.js index fca829d5c..e2df4dbb6 100644 --- a/browserid/static/dialog/test/qunit/controllers/page_controller_unit_test.js +++ b/browserid/static/dialog/test/qunit/controllers/page_controller_unit_test.js @@ -41,16 +41,21 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() { bodyTemplate = "testBodyTemplate.ejs", waitTemplate = "wait.ejs"; + function reset() { + el = $("#controller_head"); + el.find("#formWrap .contents").html(""); + el.find("#wait .contents").html(""); + el.find("#error .contents").html(""); + } + module("PageController", { setup: function() { - el = $("#page_controller"); + reset(); }, teardown: function() { - el.find("#formWrap .contents").html(""); - el.find("#wait .contents").html(""); - el.find("#error .contents").html(""); controller.destroy(); + reset(); } }); @@ -76,10 +81,11 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() { var html = el.find("#formWrap .contents").html(); ok(html.length, "with template specified, form text is loaded"); +/* var input = el.find("input").eq(0); ok(input.is(":focus"), "make sure the first input is focused"); - +*/ html = el.find("#wait .contents").html(); equal(html, "", "with body template specified, wait text is not loaded"); }); @@ -100,6 +106,22 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() { ok(html.length, "with wait template specified, wait text is loaded"); }); + test("page controller with error template renders in #error .contents", function() { + controller = el.page({ + errorTemplate: waitTemplate, + errorVars: { + title: "Test title", + message: "Test message" + } + }).controller(); + + var html = el.find("#formWrap .contents").html(); + equal(html, "", "with error template specified, form is ignored"); + + html = el.find("#error .contents").html(); + ok(html.length, "with error template specified, error text is loaded"); + }); + test("renderError renders an error message", function() { controller = el.page({ waitTemplate: waitTemplate, @@ -109,7 +131,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() { } }).controller(); - controller.renderError({ + controller.renderError("wait.ejs", { title: "error title", message: "error message" }); diff --git a/browserid/static/dialog/test/qunit/include_unit_test.js b/browserid/static/dialog/test/qunit/include_unit_test.js new file mode 100644 index 000000000..815ecf821 --- /dev/null +++ b/browserid/static/dialog/test/qunit/include_unit_test.js @@ -0,0 +1,52 @@ +/*jshint browsers:true, forin: true, laxbreak: true */ +/*global steal: true, test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID: true */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla BrowserID. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +steal.plugins("jquery", "funcunit/qunit").then("/include.js", function() { + "use strict"; + + module("include.js"); + + test("navigator.id is available", function() { + equal(typeof navigator.id, "object", "navigator.id namespace is available"); + }); + + test("navigator.id.getVerifiedEmail is available", function() { + equal(typeof navigator.id.getVerifiedEmail, "function", "navigator.id.getVerifiedEmail is available"); + }); + + +}); + diff --git a/browserid/static/dialog/test/qunit/pages/add_email_address_test.js b/browserid/static/dialog/test/qunit/pages/add_email_address_test.js new file mode 100644 index 000000000..1d0d0a016 --- /dev/null +++ b/browserid/static/dialog/test/qunit/pages/add_email_address_test.js @@ -0,0 +1,112 @@ +/*jshint browsers:true, forin: true, laxbreak: true */ +/*global steal: true, test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla BrowserID. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +steal.plugins("jquery").then("/js/pages/add_email_address", function() { + "use strict"; + + var bid = BrowserID, + network = bid.Network, + storage = bid.Storage, + emailForVerificationTokenFailure = false, + completeEmailRegistrationFailure = false, + validToken = true; + + var netMock = { + emailForVerificationToken: function(token, onSuccess, onFailure) { + emailForVerificationTokenFailure ? onFailure() : onSuccess("testuser@testuser.com"); + }, + + completeEmailRegistration: function(token, onSuccess, onFailure) { + completeEmailRegistrationFailure ? onFailure() : onSuccess(validToken); + } + }; + + module("pages/add_email_address", { + setup: function() { + BrowserID.User.setNetwork(netMock); + emailForVerificationTokenFailure = completeEmailRegistrationFailure = false; + validToken = true; + $(".error").stop().hide(); + $(".website").text(""); + }, + teardown: function() { + BrowserID.User.setNetwork(network); + $(".error").stop().hide(); + $(".website").text(""); + } + }); + + test("addEmailAddress with good token and site", function() { + storage.setStagedOnBehalfOf("browserid.org"); + + bid.addEmailAddress("token"); + + equal($("#email").text(), "testuser@testuser.com", "email set"); + ok($("#siteinfo").is(":visible"), "siteinfo is visible when we say what it is"); + equal($("#siteinfo .website").text(), "browserid.org", "origin is updated"); + }); + + test("addEmailAddress with good token and nosite", function() { + bid.addEmailAddress("token"); + + equal($("#email").text(), "testuser@testuser.com", "email set"); + equal($("#siteinfo").is(":visible"), false, "siteinfo is not visible without having it"); + equal($("#siteinfo .website").text(), "", "origin is not updated"); + }); + + test("addEmailAddress with bad token", function() { + validToken = false; + + bid.addEmailAddress("token"); + ok($("#cannotconfirm").is(":visible"), "cannot confirm box is visible"); + }); + + test("addEmailAddress with emailForVerficationToken XHR failure", function() { + validToken = true; + emailForVerificationTokenFailure = true; + bid.addEmailAddress("token"); + + ok($("#cannotcommunicate").is(":visible"), "cannot communicate box is visible"); + }); + + test("addEmailAddress with completeEmailRegistration XHR failure", function() { + validToken = true; + completeEmailRegistrationFailure = true; + bid.addEmailAddress("token"); + + ok($("#cannotcommunicate").is(":visible"), "cannot communicate box is visible"); + }); +}); diff --git a/browserid/static/dialog/test/qunit/qunit.js b/browserid/static/dialog/test/qunit/qunit.js index 435935bdb..450add4ea 100644 --- a/browserid/static/dialog/test/qunit/qunit.js +++ b/browserid/static/dialog/test/qunit/qunit.js @@ -1,4 +1,5 @@ steal("/dialog/resources/browserid.js", + "/dialog/resources/browser-support.js", "/dialog/resources/storage.js", "/dialog/resources/tooltip.js", "/dialog/resources/validation.js", @@ -12,9 +13,15 @@ steal("/dialog/resources/browserid.js", "funcunit/qunit") .views('testBodyTemplate.ejs') .views('wait.ejs') + .then("/dialog/controllers/page_controller.js") .then("browserid_unit_test") - .then("controllers/page_controller_unit_test") + .then("include_unit_test") + .then("pages/add_email_address_test") + .then("resources/browser-support_unit_test") .then("resources/validation_unit_test") .then("resources/storage_unit_test") .then("resources/network_unit_test") .then("resources/user_unit_test") + .then("controllers/page_controller_unit_test") + .then("controllers/page_controller_unit_test") + diff --git a/browserid/static/dialog/test/qunit/resources/browser-support_unit_test.js b/browserid/static/dialog/test/qunit/resources/browser-support_unit_test.js new file mode 100644 index 000000000..a26857f01 --- /dev/null +++ b/browserid/static/dialog/test/qunit/resources/browser-support_unit_test.js @@ -0,0 +1,102 @@ +/*jshint browsers:true, forin: true, laxbreak: true */ +/*global steal: true, test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID: true */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla BrowserID. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +steal.plugins("jquery", "funcunit/qunit").then(function() { + "use strict"; + + var bid = BrowserID, + support = bid.BrowserSupport, + stubWindow, + stubNavigator; + + module("browser-support", { + setup: function() { + // Hard coded goodness for testing purposes + stubNavigator = { + appName: "Netscape", + userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:7.0.1) Gecko/20100101 Firefox/7.0.1" + }; + + stubWindow = { + localStorage: {}, + postMessage: function() {} + }; + + support.setTestEnv(stubNavigator, stubWindow); + }, + + teardown: function() { + } + }); + + test("browser without localStorage", function() { + delete stubWindow.localStorage; + + equal(support.isSupported(), false, "window.localStorage is required"); + equal(support.getNoSupportReason(), "LOCALSTORAGE", "correct reason"); + }); + + + test("browser without postMessage", function() { + delete stubWindow.postMessage; + + equal(support.isSupported(), false, "window.postMessage is required"); + equal(support.getNoSupportReason(), "POSTMESSAGE", "correct reason"); + }); + + test("Fake being IE8 - unsupported intentionally", function() { + stubNavigator.appName = "Microsoft Internet Explorer"; + stubNavigator.userAgent = "MSIE 8.0"; + + equal(support.isSupported(), false, "IE8 is not supported"); + equal(support.getNoSupportReason(), "IE_VERSION", "correct reason"); + }); + + test("Fake being IE9 - supported", function() { + stubNavigator.appName = "Microsoft Internet Explorer"; + stubNavigator.userAgent = "MSIE 9.0"; + + equal(support.isSupported(), true, "IE9 is supported"); + equal(typeof support.getNoSupportReason(), "undefined", "no reason, we are all good"); + }); + + test("Firefox 7.01 with postMessage, localStorage", function() { + equal(support.isSupported(), true, "Firefox 7.01 is supported"); + equal(typeof support.getNoSupportReason(), "undefined", "no reason, we are all good"); + }); +}); + + diff --git a/browserid/static/dialog/test/qunit/resources/network_unit_test.js b/browserid/static/dialog/test/qunit/resources/network_unit_test.js index 1f532b24b..ac5b06c78 100644 --- a/browserid/static/dialog/test/qunit/resources/network_unit_test.js +++ b/browserid/static/dialog/test/qunit/resources/network_unit_test.js @@ -51,6 +51,52 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func start(); } + function notificationCheck(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); + + xhr.useResult("ajaxError"); + + var handle; + + var subscriber = function() { + ok(true, "xhr error notified application"); + wrappedStart(); + OpenAjax.hub.unsubscribe(handle); + }; + + handle = OpenAjax.hub.subscribe("xhrError", subscriber); + + if (cb) { + cb.apply(null, args); + } + + stop(); + } + + 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) { + ok(false, "XHR failure should never pass"); + wrappedStart(); + }, function onFailure() { + ok(true, "XHR failure should never pass"); + wrappedStart(); + }); + + xhr.useResult("ajaxError"); + + cb.apply(null, args); + + stop(); + } + var network = BrowserID.Network, contextInfo = { server_time: new Date().getTime(), @@ -59,33 +105,54 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func }; + /** + * This is the results table, the keys are the request type, url, and + * a "selector" for testing. The right is the expected return value, already + * decoded. If a result is "undefined", the request's error handler will be + * called. + */ var xhr = { results: { - "get /wsapi/session_context valid": contextInfo, + "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 contextAjaxError": undefined, "post /wsapi/authenticate_user valid": { success: true }, "post /wsapi/authenticate_user invalid": { success: false }, + "post /wsapi/authenticate_user ajaxError": undefined, "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 valid": { success: true }, "post /wsapi/stage_user invalid": { success: false }, + "post /wsapi/stage_user ajaxError": undefined, "get /wsapi/user_creation_status?email=address notcreated": undefined, // undefined because server returns 400 error "get /wsapi/user_creation_status?email=address pending": { status: "pending" }, "get /wsapi/user_creation_status?email=address complete": { status: "complete" }, + "get /wsapi/user_creation_status?email=address ajaxError": undefined, "post /wsapi/complete_user_creation valid": { success: true }, "post /wsapi/complete_user_creation invalid": { success: false }, + "post /wsapi/complete_user_creation ajaxError": undefined, "post /wsapi/logout valid": { success: true }, + "post /wsapi/logout ajaxError": undefined, "get /wsapi/have_email?email=address taken": { email_known: true }, "get /wsapi/have_email?email=address nottaken" : { email_known: false }, + "get /wsapi/have_email?email=address ajaxError" : undefined, "post /wsapi/remove_email valid": { success: true }, "post /wsapi/remove_email invalid": { success: false }, + "post /wsapi/remove_email ajaxError": undefined, "post /wsapi/account_cancel valid": { success: true }, "post /wsapi/account_cancel invalid": { success: false }, + "post /wsapi/account_cancel ajaxError": undefined, "post /wsapi/stage_email valid": { success: true }, "post /wsapi/stage_email invalid": { success: false }, + "post /wsapi/stage_email ajaxError": undefined, "get /wsapi/email_addition_status?email=address notcreated": undefined, // undefined because server returns 400 error "get /wsapi/email_addition_status?email=address pending": { status: "pending" }, "get /wsapi/email_addition_status?email=address complete": { status: "complete" }, + "get /wsapi/email_addition_status?email=address ajaxError": undefined }, useResult: function(result) { @@ -164,6 +231,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func stop(); }); + wrappedAsyncTest("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() { + failureCheck(network.authenticate, "testuser@testuser.com", "ajaxError"); + }); + wrappedAsyncTest("checkAuth with valid authentication", function() { contextInfo.authenticated = true; @@ -194,6 +269,26 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func }); + + wrappedAsyncTest("checkAuth with XHR failure", function() { + xhr.useResult("ajaxError"); + contextInfo.authenticated = false; + + // Do not convert this to failureCheck, we do this manually because + // checkAuth does not make an XHR request. Since it does not make an XHR + // 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(); + }, function onFailure() { + ok(false, "checkAuth does not make an ajax call, should not fail"); + wrappedStart(); + }); + + stop(); + }); + + wrappedAsyncTest("logout", function() { network.logout(function onSuccess() { ok(true, "we can logout"); @@ -207,6 +302,15 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func }); + wrappedAsyncTest("logout with XHR failure", function() { + notificationCheck(network.logout); + }); + + wrappedAsyncTest("logout with XHR failure", function() { + failureCheck(network.logout); + }); + + wrappedAsyncTest("complete_email_addition valid", function() { network.completeEmailRegistration("goodtoken", function onSuccess(proven) { equal(proven, true, "good token proved"); @@ -230,6 +334,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func stop(); }); + wrappedAsyncTest("complete_email_addition with XHR failure", function() { + notificationCheck(network.completeEmailRegistration, "goodtoken"); + }); + + wrappedAsyncTest("complete_email_addition with XHR failure", function() { + failureCheck(network.completeEmailRegistration, "goodtoken"); + }); + wrappedAsyncTest("createUser with valid user", function() { network.createUser("validuser", "origin", function onSuccess(created) { ok(created); @@ -253,6 +365,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func stop(); }); + wrappedAsyncTest("createUser with XHR failure", function() { + notificationCheck(network.createUser, "validuser", "origin"); + }); + + wrappedAsyncTest("createUser with XHR failure", function() { + failureCheck(network.createUser, "validuser", "origin"); + }); + wrappedAsyncTest("checkUserRegistration with pending email", function() { xhr.useResult("pending"); @@ -281,6 +401,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func stop(); }); + wrappedAsyncTest("checkUserRegistration with XHR failure", function() { + notificationCheck(network.checkUserRegistration, "address"); + }); + + wrappedAsyncTest("checkUserRegistration with XHR failure", function() { + failureCheck(network.checkUserRegistration, "address"); + }); + wrappedAsyncTest("completeUserRegistration with valid token", function() { network.completeUserRegistration("token", "password", function(registered) { ok(registered); @@ -295,6 +423,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func wrappedAsyncTest("completeUserRegistration with invalid token", function() { xhr.useResult("invalid"); + network.completeUserRegistration("token", "password", function(registered) { equal(registered, false); wrappedStart(); @@ -306,7 +435,16 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func stop(); }); + wrappedAsyncTest("completeUserRegistration with XHR failure", function() { + notificationCheck(network.completeUserRegistration, "token", "password"); + }); + + wrappedAsyncTest("completeUserRegistration with XHR failure", function() { + failureCheck(network.completeUserRegistration, "token", "password"); + }); + wrappedAsyncTest("cancelUser valid", function() { + network.cancelUser(function() { // XXX need a test here. ok(true); @@ -320,6 +458,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func wrappedAsyncTest("cancelUser invalid", function() { xhr.useResult("invalid"); + network.cancelUser(function() { // XXX need a test here. ok(true); @@ -331,6 +470,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func stop(); }); + wrappedAsyncTest("cancelUser with XHR failure", function() { + notificationCheck(network.cancelUser); + }); + + wrappedAsyncTest("cancelUser with XHR failure", function() { + failureCheck(network.cancelUser); + }); + wrappedAsyncTest("emailRegistered with taken email", function() { xhr.useResult("taken"); @@ -359,6 +506,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func stop(); }); + wrappedAsyncTest("emailRegistered with XHR failure", function() { + notificationCheck(network.emailRegistered, "address"); + }); + + wrappedAsyncTest("emailRegistered with XHR failure", function() { + failureCheck(network.emailRegistered, "address"); + }); + wrappedAsyncTest("addEmail valid", function() { network.addEmail("address", "origin", function onSuccess(added) { @@ -385,6 +540,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func stop(); }); + wrappedAsyncTest("addEmail with XHR failure", function() { + notificationCheck(network.addEmail, "address", "origin"); + }); + + wrappedAsyncTest("addEmail with XHR failure", function() { + failureCheck(network.addEmail, "address", "origin"); + }); + wrappedAsyncTest("checkEmailRegistration pending", function() { xhr.useResult("pending"); @@ -413,6 +576,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func stop(); }); + wrappedAsyncTest("checkEmailRegistration with XHR failure", function() { + notificationCheck(network.checkEmailRegistration, "address"); + }); + + wrappedAsyncTest("checkEmailRegistration with XHR failure", function() { + failureCheck(network.checkEmailRegistration, "address"); + }); + wrappedAsyncTest("removeEmail valid", function() { network.removeEmail("validemail", function onSuccess() { @@ -429,6 +600,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func wrappedAsyncTest("removeEmail invalid", function() { xhr.useResult("invalid"); + network.removeEmail("invalidemail", function onSuccess() { // XXX need a test here; ok(true); @@ -441,15 +613,12 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func stop(); }); - - wrappedAsyncTest("setKey", function() { - ok(true, "setKey"); - start(); + wrappedAsyncTest("removeEmail with XHR failure", function() { + notificationCheck(network.removeEmail, "validemail"); }); - wrappedAsyncTest("syncEmails", function() { - ok(true, "syncEmails"); - start(); + wrappedAsyncTest("removeEmail with XHR failure", function() { + failureCheck(network.removeEmail, "invalidemail"); }); @@ -466,6 +635,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func stop(); }); + wrappedAsyncTest("requestPasswordReset with XHR failure", function() { + notificationCheck(network.requestPasswordReset, "address", "origin"); + }); + + wrappedAsyncTest("requestPasswordReset with XHR failure", function() { + failureCheck(network.requestPasswordReset, "address", "origin"); + }); + wrappedAsyncTest("resetPassword", function() { network.resetPassword("password", function onSuccess() { // XXX need a test here; @@ -479,6 +656,23 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func stop(); }); + wrappedAsyncTest("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(); + }, function onFailure() { + ok(true, "XHR failure should always call failure"); + wrappedStart(); + }); + stop(); +*/ + start(); + }); + wrappedAsyncTest("changePassword", function() { network.changePassword("oldpassword", "newpassword", function onSuccess() { // XXX need a real wrappedAsyncTest here. @@ -492,6 +686,24 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func stop(); }); + 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(); + }); + + stop(); + */ + start(); + }); + wrappedAsyncTest("serverTime", function() { // I am forcing the server time to be 1.25 seconds off. contextInfo.server_time = new Date().getTime() - 1250; @@ -509,4 +721,35 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func stop(); }); + + wrappedAsyncTest("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() { + xhr.useResult("contextAjaxError"); + + network.serverTime(function onSuccess(time) { + ok(false, "XHR failure should never call success"); + wrappedStart(); + }, function onFailure() { + ok(true, "XHR failure should always call failure"); + wrappedStart(); + }); + + stop(); + }); + + wrappedAsyncTest("body offline message triggers offline message", function() { + OpenAjax.hub.subscribe("offline", function() { + ok(true, "offline event caught and application notified"); + start(); + }); + + $("body").trigger("offline"); + stop(); + }); }); diff --git a/browserid/static/dialog/test/qunit/resources/user_unit_test.js b/browserid/static/dialog/test/qunit/resources/user_unit_test.js index 7e0b15155..48e7a4b70 100644 --- a/browserid/static/dialog/test/qunit/resources/user_unit_test.js +++ b/browserid/static/dialog/test/qunit/resources/user_unit_test.js @@ -55,7 +55,9 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio var credentialsValid, unknownEmails, keyRefresh, syncValid, userEmails, userCheckCount = 0, emailCheckCount = 0, - registrationResponse; + registrationResponse, + xhrFailure = false, + validToken = true; var netStub = { reset: function() { @@ -64,54 +66,62 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio keyRefresh = []; userEmails = {"testuser@testuser.com": {}}; registrationResponse = "complete"; - }, - - stageUser: function(email, password, onSuccess) { - onSuccess(); + xhrFailure = false; }, checkUserRegistration: function(email, onSuccess, onFailure) { userCheckCount++; var status = userCheckCount === 2 ? registrationResponse : "pending"; - onSuccess(status); + xhrFailure ? onFailure() : onSuccess(status); + }, + + completeUserRegistration: function(token, password, onSuccess, onFailure) { + xhrFailure ? onFailure() : onSuccess(validToken); }, authenticate: function(email, password, onSuccess, onFailure) { - onSuccess(credentialsValid); + xhrFailure ? onFailure() : onSuccess(credentialsValid); }, checkAuth: function(onSuccess, onFailure) { - onSuccess(credentialsValid); + xhrFailure ? onFailure() : onSuccess(credentialsValid); }, emailRegistered: function(email, onSuccess, onFailure) { - onSuccess(email === "registered"); + xhrFailure ? onFailure() : onSuccess(email === "registered"); }, addEmail: function(email, origin, onSuccess, onFailure) { - onSuccess(true); + xhrFailure ? onFailure() : onSuccess(true); }, checkEmailRegistration: function(email, onSuccess, onFailure) { emailCheckCount++; var status = emailCheckCount === 2 ? registrationResponse : "pending"; - onSuccess(status); + xhrFailure ? onFailure() : onSuccess(status); + }, + + emailForVerificationToken: function(token, onSuccess, onFailure) { + xhrFailure ? onFailure() : onSuccess("testuser@testuser.com"); + }, + completeEmailRegistration: function(token, onSuccess, onFailure) { + xhrFailure ? onFailure() : onSuccess(validToken); }, removeEmail: function(email, onSuccess, onFailure) { - onSuccess(); + xhrFailure ? onFailure() : onSuccess(); }, listEmails: function(onSuccess, onFailure) { - onSuccess(userEmails); + xhrFailure ? onFailure() : onSuccess(userEmails); }, certKey: function(email, pubkey, onSuccess, onFailure) { if (syncValid) { - onSuccess(random_cert); + xhrFailure ? onFailure() : onSuccess(random_cert); } else { onFailure(); @@ -119,7 +129,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio }, syncEmails: function(issued_identities, onSuccess, onFailure) { - onSuccess({ + xhrFailure ? onFailure() : onSuccess({ unknown_emails: unknownEmails, key_refresh: keyRefresh }); @@ -127,36 +137,36 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio setKey: function(email, keypair, onSuccess, onFailure) { if (syncValid) { - onSuccess(); + xhrFailure ? onFailure() : onSuccess(); } else { onFailure(); } }, - createUser: function(email, origin, onSuccess) { - onSuccess(true); + createUser: function(email, origin, onSuccess, onFailure) { + xhrFailure ? onFailure() : onSuccess(true); }, setPassword: function(password, onSuccess) { - onSuccess(); + xhrFailure ? onFailure() : onSuccess(); }, requestPasswordReset: function(email, origin, onSuccess, onFailure) { - onSuccess(true); + xhrFailure ? onFailure() : onSuccess(true); }, - cancelUser: function(onSuccess) { - onSuccess(); + cancelUser: function(onSuccess, onFailure) { + xhrFailure ? onFailure() : onSuccess(); }, - serverTime: function(onSuccess) { - onSuccess(new Date()); + serverTime: function(onSuccess, onFailure) { + xhrFailure ? onFailure() : onSuccess(new Date()); }, - logout: function(onSuccess) { + logout: function(onSuccess, onFailure) { credentialsValid = false; - onSuccess(); + xhrFailure ? onFailure() : onSuccess(); } }; @@ -203,6 +213,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio netStub.reset(); userCheckCount = 0; emailCheckCount = 0; + validToken = true; }, teardown: function() { lib.setNetwork(BrowserID.Network); @@ -273,6 +284,20 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio stop(); }); + test("createUser with XHR failure", function() { + xhrFailure = true; + + lib.createUser("testuser@testuser.com", function(status) { + ok(false, "xhr failure should never succeed"); + start(); + }, function() { + ok(true, "xhr failure should always be a failure"); + start(); + }); + + stop(); + }); + /** * The next three tests use the mock network harness. The tests are testing * the polling action and whether `waitForUserValidation` reacts as expected @@ -283,8 +308,12 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio * stored in `registrationResponse`. */ test("waitForUserValidation with `complete` response", function() { + storage.setStagedOnBehalfOf(testOrigin); + lib.waitForUserValidation("testuser@testuser.com", function(status) { equal(status, "complete", "complete response expected"); + + ok(!storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes"); start(); }, failure("waitForUserValidation failure")); @@ -294,8 +323,12 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio test("waitForUserValidation with `mustAuth` response", function() { registrationResponse = "mustAuth"; + storage.setStagedOnBehalfOf(testOrigin); + lib.waitForUserValidation("testuser@testuser.com", function(status) { equal(status, "mustAuth", "mustAuth response expected"); + + ok(!storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes"); start(); }, failure("waitForUserValidation failure")); @@ -305,10 +338,13 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio test("waitForUserValidation with `noRegistration` response", function() { registrationResponse = "noRegistration"; + storage.setStagedOnBehalfOf(testOrigin); lib.waitForUserValidation("baduser@testuser.com", function(status) { ok(false, "not expecting success") + start(); }, function(status) { + ok(storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes"); ok(status, "noRegistration", "noRegistration response causes failure"); start(); }); @@ -316,6 +352,65 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio stop(); }); + test("waitForUserValidation with XHR failure", function() { + xhrFailure = true; + + storage.setStagedOnBehalfOf(testOrigin); + lib.waitForUserValidation("baduser@testuser.com", function(status) { + ok(false, "xhr failure should never succeed"); + start(); + }, function() { + ok(storage.getStagedOnBehalfOf(), "staged on behalf of is not cleared on XHR failure"); + ok(true, "xhr failure should always be a failure"); + start(); + }); + + stop(); + }); + + test("verifyUser with a good token", function() { + storage.setStagedOnBehalfOf(testOrigin); + lib.verifyUser("token", "password", function onSuccess(info) { + + ok(info.valid, "token was valid"); + equal(info.email, "testuser@testuser.com", "email part of info"); + equal(info.origin, testOrigin, "origin in info"); + equal(storage.getStagedOnBehalfOf(), "", "initiating origin was removed"); + + start(); + }, failure("verifyUser failure")); + + stop(); + }); + + test("verifyUser with a bad token", function() { + validToken = false; + + lib.verifyUser("token", "password", function onSuccess(info) { + + equal(info.valid, false, "bad token calls onSuccess with a false validity"); + + start(); + }, failure("verifyUser failure")); + + stop(); + + }); + + test("verifyUser with an XHR failure", function() { + xhrFailure = true; + + lib.verifyUser("token", "password", function onSuccess(info) { + ok(false, "xhr failure should never succeed"); + start(); + }, function() { + ok(true, "xhr failure should always be a failure"); + start(); + }); + + stop(); + }); + test("setPassword", function() { lib.setPassword("password", function() { // XXX fill this in. @@ -348,7 +443,6 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio }); - test("authenticate with invalid credentials", function() { credentialsValid = false; lib.authenticate("testuser@testuser.com", "testuser", function onComplete(authenticated) { @@ -361,6 +455,21 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio }); + test("authenticate with XHR failure", function() { + xhrFailure = true; + lib.authenticate("testuser@testuser.com", "testuser", function onComplete(authenticated) { + ok(false, "xhr failure should never succeed"); + start(); + }, function() { + ok(true, "xhr failure should always be a failure"); + start(); + }); + + stop(); + + }); + + test("checkAuthentication with valid authentication", function() { credentialsValid = true; lib.checkAuthentication(function(authenticated) { @@ -385,6 +494,21 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio + test("checkAuthentication with XHR failure", function() { + xhrFailure = true; + lib.checkAuthentication(function(authenticated) { + ok(false, "xhr failure should never succeed"); + start(); + }, function() { + ok(true, "xhr failure should always be a failure"); + start(); + }); + + stop(); + }); + + + test("checkAuthenticationAndSync with valid authentication", function() { credentialsValid = true; lib.checkAuthenticationAndSync(function onSuccess() {}, @@ -411,6 +535,24 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio stop(); }); + + test("checkAuthenticationAndSync with XHR failure", function() { + xhrFailure = true; + lib.checkAuthenticationAndSync(function onSuccess() { + ok(false, "xhr failure should never succeed"); + }, function onComplete() { + ok(false, "xhr failure should never succeed"); + + start(); + }, function onFailure() { + ok(true, "xhr failure should always be a failure"); + start(); + }); + + stop(); + }); + + test("isEmailRegistered with registered email", function() { lib.isEmailRegistered("registered", function(registered) { ok(registered); @@ -435,6 +577,19 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio stop(); }); + test("isEmailRegistered with XHR failure", function() { + xhrFailure = true; + lib.isEmailRegistered("registered", function(registered) { + ok(false, "xhr failure should never succeed"); + start(); + }, function() { + ok(true, "xhr failure should always be a failure"); + start(); + }); + + stop(); + }); + test("addEmail", function() { lib.addEmail("testemail@testemail.com", function(added) { ok(added, "user was added"); @@ -442,12 +597,29 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio var identities = lib.getStoredEmailKeypairs(); equal(false, "testemail@testemail.com" in identities, "Our new email is not added until confirmation."); + + equal(storage.getStagedOnBehalfOf(), lib.getHostname(), "initiatingOrigin is stored"); + start(); }, failure("addEmail failure")); stop(); }); + test("addEmail with XHR failure", function() { + xhrFailure = true; + lib.addEmail("testemail@testemail.com", function(added) { + ok(false, "xhr failure should never succeed"); + start(); + }, function() { + ok(true, "xhr failure should always be a failure"); + start(); + }); + + stop(); + }); + + /** * The next three tests use the mock network harness. The tests are testing @@ -459,7 +631,10 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio * stored in `registrationResponse`. */ test("waitForEmailValidation `complete` response", function() { + storage.setStagedOnBehalfOf(testOrigin); + lib.waitForEmailValidation("testemail@testemail.com", function(status) { + ok(!storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes"); equal(status, "complete", "complete response expected"); start(); }, failure("waitForEmailValidation failure")); @@ -468,9 +643,11 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio }); test("waitForEmailValidation `mustAuth` response", function() { + storage.setStagedOnBehalfOf(testOrigin); registrationResponse = "mustAuth"; lib.waitForEmailValidation("testemail@testemail.com", function(status) { + ok(!storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes"); equal(status, "mustAuth", "mustAuth response expected"); start(); }, failure("waitForEmailValidation failure")); @@ -479,12 +656,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio }); test("waitForEmailValidation with `noRegistration` response", function() { + storage.setStagedOnBehalfOf(testOrigin); registrationResponse = "noRegistration"; lib.waitForEmailValidation("baduser@testuser.com", function(status) { ok(false, "not expecting success") start(); }, function(status) { + ok(storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes"); ok(status, "noRegistration", "noRegistration response causes failure"); start(); }); @@ -492,6 +671,67 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio stop(); }); + + test("waitForEmailValidation XHR failure", function() { + storage.setStagedOnBehalfOf(testOrigin); + xhrFailure = true; + + lib.waitForEmailValidation("testemail@testemail.com", function(status) { + ok(false, "xhr failure should never succeed"); + start(); + }, function() { + ok(storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes"); + ok(true, "xhr failure should always be a failure"); + start(); + }); + + stop(); + }); + + + test("verifyEmail with a good token", function() { + storage.setStagedOnBehalfOf(testOrigin); + lib.verifyEmail("token", function onSuccess(info) { + + ok(info.valid, "token was valid"); + equal(info.email, "testuser@testuser.com", "email part of info"); + equal(info.origin, testOrigin, "origin in info"); + equal(storage.getStagedOnBehalfOf(), "", "initiating origin was removed"); + + start(); + }, failure("verifyEmail failure")); + + stop(); + }); + + test("verifyEmail with a bad token", function() { + validToken = false; + + lib.verifyEmail("token", function onSuccess(info) { + + equal(info.valid, false, "bad token calls onSuccess with a false validity"); + + start(); + }, failure("verifyEmail failure")); + + stop(); + + }); + + test("verifyEmail with an XHR failure", function() { + xhrFailure = true; + + lib.verifyEmail("token", function onSuccess(info) { + ok(false, "xhr failure should never succeed"); + start(); + }, function() { + ok(true, "xhr failure should always be a failure"); + start(); + }); + + stop(); + }); + test("syncEmailKeypair with successful sync", function() { syncValid = true; lib.syncEmailKeypair("testemail@testemail.com", function(keypair) { @@ -523,6 +763,19 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio stop(); }); + test("syncEmailKeypair with XHR failure", function() { + xhrFailure = true; + lib.syncEmailKeypair("testemail@testemail.com", function(keypair) { + ok(false, "xhr failure should never succeed"); + start(); + }, function() { + ok(true, "xhr failure should always be a failure"); + start(); + }); + + stop(); + }); + test("removeEmail that is added", function() { storage.addEmail("testemail@testemail.com", {pub: "pub", priv: "priv"}); @@ -548,6 +801,22 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio stop(); }); + test("removeEmail with XHR failure", function() { + storage.addEmail("testemail@testemail.com", {pub: "pub", priv: "priv"}); + + xhrFailure = true; + lib.removeEmail("testemail@testemail.com", function() { + ok(false, "xhr failure should never succeed"); + start(); + }, function() { + ok(true, "xhr failure should always be a failure"); + start(); + }); + + stop(); + }); + + test("syncEmails with no pre-loaded identities and no identities to add", function() { @@ -636,8 +905,22 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio stop(); }); + test("syncEmails with XHR failure", function() { + xhrFailure = true; + + lib.syncEmails(function onSuccess() { + ok(false, "xhr failure should never succeed"); + start(); + }, function() { + ok(true, "xhr failure should always be a failure"); + start(); + }); + + stop(); + }); test("getAssertion with known email that has key", function() { + lib.setOrigin(testOrigin); lib.syncEmailKeypair("testuser@testuser.com", function() { lib.getAssertion("testuser@testuser.com", function onSuccess(assertion) { testAssertion(assertion); @@ -650,6 +933,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio test("getAssertion with known email that does not have a key", function() { + lib.setOrigin(testOrigin); storage.addEmail("testuser@testuser.com", {}); lib.getAssertion("testuser@testuser.com", function onSuccess(assertion) { testAssertion(assertion); @@ -671,6 +955,22 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio stop(); }); + test("getAssertion with XHR failure", function() { + lib.setOrigin(testOrigin); + xhrFailure = true; + + lib.syncEmailKeypair("testuser@testuser.com", function() { + ok(false, "xhr failure should never succeed"); + start(); + }, function() { + ok(true, "xhr failure should always be a failure"); + start(); + }); + + stop(); + }); + + test("logoutUser", function(onSuccess) { credentialsValid = true; keyRefresh = ["testuser@testuser.com"]; @@ -693,6 +993,29 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio stop(); }); + test("logoutUser with XHR failure", function(onSuccess) { + credentialsValid = true; + keyRefresh = ["testuser@testuser.com"]; + + lib.authenticate("testuser@testuser.com", "testuser", function(authenticated) { + lib.syncEmails(function() { + xhrFailure = true; + + lib.logoutUser(function() { + ok(false, "xhr failure should never succeed"); + start(); + }, function() { + ok(true, "xhr failure should always be a failure"); + start(); + }); + + + }, failure("syncEmails failure")); + }, failure("authenticate failure")); + + stop(); + }); + test("cancelUser", function(onSuccess) { lib.cancelUser(function() { var storedIdentities = storage.getEmails(); @@ -703,4 +1026,17 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio stop(); }); + test("cancelUser with XHR failure", function(onSuccess) { + xhrFailure = true; + lib.cancelUser(function() { + ok(false, "xhr failure should never succeed"); + start(); + }, function() { + ok(true, "xhr failure should always be a failure"); + start(); + }); + + stop(); + }); + }); diff --git a/browserid/static/i/firefox_logo.png b/browserid/static/i/firefox_logo.png new file mode 100644 index 000000000..c55a33808 Binary files /dev/null and b/browserid/static/i/firefox_logo.png differ diff --git a/browserid/static/include.js b/browserid/static/include.js index 9f16d5d47..cebeafc44 100644 --- a/browserid/static/include.js +++ b/browserid/static/include.js @@ -557,55 +557,88 @@ }; })(); - function getInternetExplorerVersion() { - var rv = -1; // Return value assumes failure. - if (navigator.appName == 'Microsoft Internet Explorer') { - var ua = navigator.userAgent; - var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); - if (re.exec(ua) != null) - rv = parseFloat(RegExp.$1); + var BrowserSupport = (function() { + var win = window, + nav = navigator, + reason; + + // For unit testing + function setTestEnv(newNav, newWindow) { + nav = newNav; + win = newWindow; } - return rv; - } - - function checkIE() { - var ieVersion = getInternetExplorerVersion(), - ieNosupport = ieVersion > -1 && ieVersion < 9, - message; + function getInternetExplorerVersion() { + var rv = -1; // Return value assumes failure. + if (nav.appName == 'Microsoft Internet Explorer') { + var ua = nav.userAgent; + var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); + if (re.exec(ua) != null) + rv = parseFloat(RegExp.$1); + } - if(ieNosupport) { - message = "Unfortunately, your version of Internet Explorer is not yet supported.\n" + - 'If you are using Internet Explorer 9, turn off "Compatibility View".'; + return rv; } - return message; - } + function checkIE() { + var ieVersion = getInternetExplorerVersion(), + ieNosupport = ieVersion > -1 && ieVersion < 9; - function explicitNosupport() { - var message = checkIE(); + if(ieNosupport) { + return "IE_VERSION"; + } + } - if (message) { - message += "\nWe are working hard to bring BrowserID support to your browser!"; - alert(message); + function explicitNosupport() { + return checkIE(); } - return message; - } + function checkLocalStorage() { + var localStorage = 'localStorage' in win && win['localStorage'] !== null; + if(!localStorage) { + return "LOCALSTORAGE"; + } + } - function checkRequirements() { - var localStorage = 'localStorage' in window && window['localStorage'] !== null; - var postMessage = !!window.postMessage; - var json = true; + function checkPostMessage() { + if(!win.postMessage) { + return "POSTMESSAGE"; + } + } - var explicitNo = explicitNosupport() + function isSupported() { + reason = checkLocalStorage() || checkPostMessage() || explicitNosupport(); - if(!explicitNo && !(localStorage && postMessage && json)) { - alert("Unfortunately, your browser does not meet the minimum HTML5 support required for BrowserID."); + return !reason; } - return localStorage && postMessage && json && !(explicitNo); - } + function getNoSupportReason() { + return reason; + } + + return { + /** + * Set the test environment. + * @method setTestEnv + */ + setTestEnv: setTestEnv, + /** + * Check whether the current browser is supported + * @method isSupported + * @returns {boolean} + */ + isSupported: isSupported, + /** + * Called after isSupported, if isSupported returns false. Gets the reason + * why browser is not supported. + * @method getNoSupportReason + * @returns {string} + */ + getNoSupportReason: getNoSupportReason + }; + + }()); + // this is for calls that are non-interactive function _open_hidden_iframe(doc) { @@ -636,16 +669,20 @@ return iframe; } - function _open_window() { + function _open_window(url) { + url = url || "about:blank"; // we open the window initially blank, and only after our relay frame has // been constructed do we update the location. This is done because we // must launch the window inside a click handler, but we should wait to // start loading it until our relay iframe is instantiated and ready. // see issue #287 & #286 - return window.open( - "about:blank", + var dialog = window.open( + url, "_mozid_signin", isFennec ? undefined : "menubar=0,location=0,resizable=0,scrollbars=0,status=0,dialog=1,width=700,height=375"); + + dialog.focus(); + return dialog; } function _attach_event(element, name, listener) { @@ -684,16 +721,17 @@ // keep track of these so that we can re-use/re-focus an already open window. navigator.id.getVerifiedEmail = function(callback) { - if(!checkRequirements()) { - return; - } - if (w) { // if there is already a window open, just focus the old window. w.focus(); return; } + if (!BrowserSupport.isSupported()) { + w = _open_window(ipServer + "/unsupported_dialog"); + return; + } + var frameid = _get_relayframe_id(); var iframe = _open_relayframe("browserid_relay_" + frameid); w = _open_window(); @@ -712,8 +750,7 @@ // has a problem re-attaching new iframes with the same name. Code inside // of frames with the same name sometimes does not get run. // See https://bugzilla.mozilla.org/show_bug.cgi?id=350023 - w.location = ipServer + "/sign_in#" + frameid; - w.focus(); + w = _open_window(ipServer + "/sign_in#" + frameid); } }); @@ -721,8 +758,10 @@ chan.destroy(); chan = null; - w.close(); - w = null; + if (w) { + w.close(); + w = null; + } iframe.parentNode.removeChild(iframe); iframe = null; diff --git a/browserid/static/js/pages/add_email_address.js b/browserid/static/js/pages/add_email_address.js index e5c91d305..58b4198f6 100644 --- a/browserid/static/js/pages/add_email_address.js +++ b/browserid/static/js/pages/add_email_address.js @@ -36,28 +36,35 @@ (function() { "use strict"; + + var ANIMATION_TIME=250; + function emailRegistrationSuccess(info) { - function emailRegistrationSuccess() { - $(".hint").hide(); - $("#congrats").fadeIn(250, function() { - $("body").delay(1000).fadeOut(500, function() { - // if the close didn't work, then let's redirect the the main page where they'll - // get to see the ids that they've created. - document.location = '/'; - }); + $("#email").text(info.email); + + if (info.origin) { + $("#siteinfo .website").html(info.origin); + $("#siteinfo").show(); + } + + $("#signUpForm").delay(2000).fadeOut(ANIMATION_TIME, function() { + $("#congrats").fadeIn(ANIMATION_TIME); }); } function showError(el) { $(".hint").hide(); - $(el).fadeIn(250); + $(el).fadeIn(ANIMATION_TIME); } BrowserID.addEmailAddress = function(token) { - BrowserID.Network.completeEmailRegistration(token, function onSuccess(valid) { - if (valid) { - emailRegistrationSuccess(); - } else { + var user = BrowserID.User; + + user.verifyEmail(token, function onSuccess(info) { + if (info.valid) { + emailRegistrationSuccess(info); + } + else { showError("#cannotconfirm"); } }, function onFailure() { diff --git a/browserid/static/ping.txt b/browserid/static/ping.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/browserid/tests/lib/start-stop.js b/browserid/tests/lib/start-stop.js index f9e69b8a5..d8a837f7e 100644 --- a/browserid/tests/lib/start-stop.js +++ b/browserid/tests/lib/start-stop.js @@ -71,7 +71,7 @@ exports.addStartupBatches = function(suite) { return true; }, "server should be running": { - topic: wsapi.get('/ping.txt'), + topic: wsapi.get('/__heartbeat__'), "server is running": function (r, err) { assert.equal(r.code, 200); } diff --git a/browserid/tests/page-requests-test.js b/browserid/tests/page-requests-test.js new file mode 100755 index 000000000..156a0611d --- /dev/null +++ b/browserid/tests/page-requests-test.js @@ -0,0 +1,116 @@ +#!/usr/bin/env node + +/* ***** 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 ***** */ + +require('./lib/test_env.js'); + +const assert = require('assert'), +http = require('http'), +vows = require('vows'), +start_stop = require('./lib/start-stop.js'), +wsapi = require('./lib/wsapi.js'); + +var suite = vows.describe('page requests'); + +// start up a pristine server +start_stop.addStartupBatches(suite); + +// This set of tests check to make sure all of the expected pages are served +// up with the correct status codes. We use Lloyd's wsapi client as our REST +// interface. + + + +// Taken from the vows page. +function assertStatus(code) { + return function (res, err) { + assert.equal(res.code, code); + }; +} + +function respondsWith(status) { + var context = { + topic: function () { + // Get the current context's name, such as "POST /" + // and split it at the space. + var req = this.context.name.split(/ +/), // ["POST", "/"] + method = req[0].toLowerCase(), // "post" + path = req[1]; // "/" + + // Perform the contextual client request, + // with the above method and path. + wsapi[method](path).call(this); + } + }; + + // Create and assign the vow to the context. + // The description is generated from the expected status code + // and the status name, from node's http module. + context['should respond with a ' + status + ' ' + + http.STATUS_CODES[status]] = assertStatus(status); + + return context; +} + +suite.addBatch({ + 'GET /': respondsWith(200), + 'GET /signup': respondsWith(200), + 'GET /forgot': respondsWith(200), + 'GET /signin': respondsWith(200), + 'GET /about': respondsWith(200), + 'GET /tos': respondsWith(200), + 'GET /privacy': respondsWith(200), + 'GET /verify_email_address': respondsWith(200), + 'GET /add_email_address': respondsWith(200), + 'GET /pk': respondsWith(200), + 'GET /vepbundle': respondsWith(200), + 'GET /signin': respondsWith(200), + 'GET /unsupported_dialog': respondsWith(200), + 'GET /developers': respondsWith(200), + 'GET /manage': respondsWith(302), + 'GET /users': respondsWith(302), + 'GET /users/': respondsWith(302), + 'GET /primaries': respondsWith(302), + 'GET /primaries/': respondsWith(302), + 'GET /developers': respondsWith(302) +}); + +// shut the server down and cleanup +start_stop.addShutdownBatches(suite); + +// run or export the suite. +if (process.argv[1] === __filename) suite.run(); +else suite.export(module); diff --git a/browserid/views/dialog.ejs b/browserid/views/dialog.ejs index 4bde9a26a..d2b634a26 100644 --- a/browserid/views/dialog.ejs +++ b/browserid/views/dialog.ejs @@ -1,90 +1,35 @@ - - - - - - - <% if (production) { %> - - <% } else { %> - - - <% } %> - - Browser ID - - -
      - - -
      -
      -
      -
      -
      - -
      -
      - -
      -
      -
      -
      -
      -
      -
      -
      -
      - - -
      -
      -
      -

      Communicating with server

      -

      Just a moment while we talk with the server.

      -
      -
      -
      - - -
      -
      -
      -
      -
      -
      -
      - - +
      +
      +
      +

      Communicating with server

      +

      Just a moment while we talk with the server.

      +
      +
      +
      -
      - - - - - + diff --git a/browserid/views/dialog_layout.ejs b/browserid/views/dialog_layout.ejs new file mode 100644 index 000000000..13dd4d98f --- /dev/null +++ b/browserid/views/dialog_layout.ejs @@ -0,0 +1,58 @@ + + + + + + + <% if (production) { %> + + <% } else { %> + + + <% } %> + + Browser ID + + +
      + + +
      + <%- body %> +
      + + + +
      + + <% if (useJavascript !== false) { %> + + + + <% } %> + + diff --git a/browserid/views/unsupported_dialog.ejs b/browserid/views/unsupported_dialog.ejs new file mode 100644 index 000000000..acf52333b --- /dev/null +++ b/browserid/views/unsupported_dialog.ejs @@ -0,0 +1,28 @@ +
      +
      +
      +
      + We're sorry, but currently your browser isn't supported. +
      + +
      + +
      + + Firefox logo + + +

      + BrowserID works with Firefox +

      + +

      + and other modern browsers. +

      +
      + +
      + +
      +
      +
      diff --git a/browserid/views/verifyemail.ejs b/browserid/views/verifyemail.ejs index 766c84b76..b4f4e0eb0 100644 --- a/browserid/views/verifyemail.ejs +++ b/browserid/views/verifyemail.ejs @@ -2,17 +2,27 @@

      Email Verification

      +
      • Error comunicating with server.
      • Error encountered while attempting to confirm your address. Have you previously verified this address?
      +

      One moment while we attempt to confirm your email address...

      -
      +
      + +
      +

      + Your address has been verified! -

      - Your email address has been verified! + + Your new address is set up and you should now be signed in. + You may now close this window and go back to + + +

      -
      +
      diff --git a/libs/configuration.js b/libs/configuration.js index ab43bbcb5..54a242378 100644 --- a/libs/configuration.js +++ b/libs/configuration.js @@ -78,7 +78,8 @@ g_configs.production = { var_path: '/home/browserid/var/', database: { driver: "mysql", - user: 'browserid' + user: 'browserid', + create_schema: true }, bcrypt_work_factor: 12, authentication_duration_ms: (7 * 24 * 60 * 60 * 1000), @@ -109,6 +110,11 @@ g_configs.local = { certificate_validity_ms: g_configs.production.certificate_validity_ms }; +if (undefined !== process.env['NODE_EXTRA_CONFIG']) { + var fs = require('fs'); + eval(fs.readFileSync(process.env['NODE_EXTRA_CONFIG']) + ''); +} + Object.keys(g_configs).forEach(function(config) { if (!g_configs[config].smtp) { g_configs[config].smtp = { @@ -148,12 +154,13 @@ g_config['URL'] = g_config['scheme'] + '://' + g_config['hostname'] + getPortFor * to re-write urls as needed for this particular environment. * * Note, for a 'local' environment, no re-write is needed because this is - * handled at a higher level. For a 'production' env no rewrite is necc cause - * all source files are written for that environment. + * handled at a higher level. For other environments, only perform re-writing + * if the host, port, or scheme are different than https://browserid.org:443 + * (all source files always should have the production hostname written into them) */ exports.performSubstitution = function(app) { - if (process.env['NODE_ENV'] !== 'production' && - process.env['NODE_ENV'] !== 'local') { + if ((g_config.hostname != 'browserid.org' || g_config.port != '443' || g_config.scheme != 'https') && + process.env['NODE_ENV'] !== 'local'){ app.use(substitution.substitute({ 'https://browserid.org': g_config['URL'], 'browserid.org:443': g_config['hostname'] + ':' + g_config['port'], @@ -164,7 +171,7 @@ exports.performSubstitution = function(app) { // At the time this file is required, we'll determine the "process name" for this proc // if we can determine what type of process it is (browserid or verifier) based -// on the path, we'll use that, otherwise we'll name it 'ephemeral'. +// on the path, we'll use that, otherwise we'll name it 'ephemeral'. if (process.argv[1] == path.join(__dirname, "..", "browserid", "run.js")) { g_config['process_type'] = 'browserid'; } else if (process.argv[1] == path.join(__dirname, "..", "verifier", "run.js")) { diff --git a/libs/heartbeat.js b/libs/heartbeat.js new file mode 100644 index 000000000..c5f2aaf70 --- /dev/null +++ b/libs/heartbeat.js @@ -0,0 +1,7 @@ +exports.setup = function(app) { + app.get("/__heartbeat__", function(req, res) { + res.writeHead(200); + res.write('ok'); + res.end(); + }); +}; diff --git a/libs/secrets.js b/libs/secrets.js index 46ea829f2..0b404f615 100644 --- a/libs/secrets.js +++ b/libs/secrets.js @@ -57,6 +57,8 @@ exports.hydrateSecret = function(name, dir) { if (secret === undefined) { secret = exports.generate(128); + fs.writeFileSync(p, ''); + fs.chmodSync(p, 0600); fs.writeFileSync(p, secret); } return secret; diff --git a/scripts/rpmbuild.sh b/scripts/rpmbuild.sh new file mode 100755 index 000000000..c7caae8cd --- /dev/null +++ b/scripts/rpmbuild.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +set -e + +progname=$(basename $0) + +cd $(dirname $0)/.. # top level of the checkout + +curdir=$(basename $PWD) +if [ "$curdir" != "browserid" ]; then + echo "$progname: git checkout must be in a dir named 'browserid'" >&2 + exit 1 +fi + +mkdir -p rpmbuild/SOURCES rpmbuild/SPECS +rm -rf rpmbuild/RPMS + +tar -C .. --exclude rpmbuild -czf \ + $PWD/rpmbuild/SOURCES/browserid-server.tar.gz browserid + +set +e + +rpmbuild --define "_topdir $PWD/rpmbuild" -ba browserid.spec +rc=$? +if [ $rc -eq 0 ]; then + ls -l $PWD/rpmbuild/RPMS/*/*.rpm +else + echo "$progname: failed to build browserid RPM (rpmbuild rc=$rc)" >&2 +fi + +exit $rc diff --git a/scripts/update_database.sql b/scripts/update_database.sql new file mode 100644 index 000000000..976d85f1b --- /dev/null +++ b/scripts/update_database.sql @@ -0,0 +1,18 @@ +ALTER TABLE user ENGINE = InnoDB; +ALTER TABLE user CHANGE id id BIGINT AUTO_INCREMENT; +ALTER TABLE user CHANGE passwd passwd CHAR(64) NOT NULL; + +ALTER TABLE email ENGINE = InnoDB; +ALTER TABLE email CHANGE id id BIGINT AUTO_INCREMENT; +ALTER TABLE email CHANGE user user BIGINT NOT NULL; +ALTER TABLE email ADD FOREIGN KEY user_fkey (user) REFERENCES user(id); +ALTER TABLE email CHANGE address address VARCHAR(255) UNIQUE NOT NULL; + +ALTER TABLE staged ENGINE = InnoDB; +ALTER TABLE staged DROP PRIMARY KEY; +ALTER TABLE staged ADD id BIGINT AUTO_INCREMENT PRIMARY KEY; +ALTER TABLE staged CHANGE secret secret CHAR(48) UNIQUE NOT NULL; +ALTER TABLE staged CHANGE new_acct new_acct BOOL NOT NULL; +ALTER TABLE staged CHANGE email email VARCHAR(255) UNIQUE NOT NULL; +ALTER TABLE staged CHANGE ts ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL; + diff --git a/test.sh b/test.sh deleted file mode 100755 index b41801ed1..000000000 --- a/test.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -npm test diff --git a/verifier/app.js b/verifier/app.js index 42d266195..9f1322e4f 100644 --- a/verifier/app.js +++ b/verifier/app.js @@ -34,13 +34,14 @@ * * ***** END LICENSE BLOCK ***** */ -const path = require('path'), - url = require('url'), - fs = require('fs'), +const path = require('path'), + url = require('url'), + fs = require('fs'), certassertion = require('./lib/certassertion.js'), - express = require('express'), - metrics = require('../libs/metrics.js'), - logger = require('../libs/logging.js').logger; + express = require('express'), + metrics = require('../libs/metrics.js'), + heartbeat = require('../libs/heartbeat.js'), + logger = require('../libs/logging.js').logger; logger.info("verifier server starting up"); @@ -114,12 +115,8 @@ exports.setup = function(app) { process.exit(); }); - // A simple ping hook for monitoring. - app.get("/ping.txt", function(req ,resp) { - resp.writeHead(200, {"Content-Type": "text/plain"}) - resp.write("k."); - resp.end(); - }); + // setup health check / heartbeat + heartbeat.setup(app); app.post('/', doVerify); app.post('/verify', doVerify); diff --git a/verifier/lib/certassertion.js b/verifier/lib/certassertion.js index fb88dbd33..71590eb62 100644 --- a/verifier/lib/certassertion.js +++ b/verifier/lib/certassertion.js @@ -58,6 +58,8 @@ var publicKeys = {}; // set up some default public keys publicKeys[configuration.get('hostname')] = secrets.PUBLIC_KEY; +logger.debug("pre-seeded public key cache with key for " + + configuration.get('hostname')); function https_complete_get(host, url, successCB, errorCB) { https.get({host: host,path: url}, function(res) { @@ -78,10 +80,16 @@ function https_complete_get(host, url, successCB, errorCB) { // only over SSL function retrieveHostPublicKey(host, successCB, errorCB) { + logger.debug("attempting to fetching public key for " + host); + // cached? var cached = publicKeys[host]; - if (cached) + if (cached) { + logger.debug("public key for " + host + " returned from cache"); return successCB(cached); + } + + logger.debug("performing HTTP request to fetch public key from " + host); https_complete_get(host, HOSTMETA_URL, function(hostmeta) { // find the location of the public key @@ -216,4 +224,4 @@ function verify(assertion, audience, successCB, errorCB, pkRetriever) { exports.retrieveHostPublicKey = retrieveHostPublicKey; -exports.verify = verify; \ No newline at end of file +exports.verify = verify;