diff --git a/lib/browserid/views.js b/lib/browserid/views.js index fb61705937f09cd74d67686e0d8de7fd151139dd..0ca111100a1211e636c1e17cda6a818486223e0b 100644 --- a/lib/browserid/views.js +++ b/lib/browserid/views.js @@ -10,7 +10,8 @@ fs = require('fs'), connect = require('connect'), config = require('../configuration.js'), und = require('underscore'), -util = require('util'); +util = require('util'), +httputils = require('../httputils.js'); // all templated content, redirects, and renames are handled here. // anything that is not an api, and not static @@ -154,18 +155,19 @@ exports.setup = function(app) { renderCachableView(req, res, 'add_email_address.ejs', {title: 'Verify Email Address', fullpage: false}); }); - /** - * - * XXX benadida or lloyd, I tried to use straight up regexp to do this, but. - * is there a better way to do this? - */ - function QUnit(req, res) { - res.render('test.ejs', {title: 'BrowserID QUnit Test', layout: false}); + // serve up testing templates. but NOT in staging or production. see GH-1044 + if ([ 'https://browserid.org', 'https://diresworb.org' ].indexOf(config.get('public_url')) === -1) { + // serve test.ejs to /test or /test/ or /test/index.html + app.get(/^\/test\/(?:index.html)?$/, function (req, res) { + res.render('test.ejs', {title: 'BrowserID QUnit Test', layout: false}); + }); + } else { + // this is stage or production, explicitly disable all resources under /test + app.get(/^\/test/, function(req, res) { + httputils.notFound("Cannot " + req.method + " " + req.url); + }); } - app.get("/test", QUnit); - app.get("/test/index.html", QUnit); - // REDIRECTS REDIRECTS = { "/manage": "/", diff --git a/resources/static/css/style.css b/resources/static/css/style.css index 0dfb71a30495e00c7a74da99a02c8c5b1a1df328..2b75378bdbc0a133fc700aa3f456892252a6f32d 100644 --- a/resources/static/css/style.css +++ b/resources/static/css/style.css @@ -808,3 +808,18 @@ footer { bottom: 0; } +#newsbanner { + background-color: #faca33; + line-height: 32px; + border-radius: 4px; + margin-bottom: 20px; + text-align: center; + color: #626160; + text-shadow: 1px 1px 0 rgba(255,255,255,0.5); + height: 32px; + -webkit-transition: all 500ms; + -moz-transition: all 500ms; + -ms-transition: all 500ms; + -o-transition: all 500ms; + transition: all 500ms; +} diff --git a/resources/static/dialog/controllers/pick_email.js b/resources/static/dialog/controllers/pick_email.js index e68d404a3a7ff1a6941c6c1a1e5ef978c5bf364c..4cdd0a1d376b41637dcb0341e911ab9166774b39 100644 --- a/resources/static/dialog/controllers/pick_email.js +++ b/resources/static/dialog/controllers/pick_email.js @@ -64,10 +64,22 @@ BrowserID.Modules.PickEmail = (function() { return identities; } - function selectEmail(event) { - var target = dom.getAttr(event.currentTarget, "for"); - if(target) { - dom.setAttr("#" + target, "checked", "checked"); + function proxyEventToInput(event) { + // iOS will not select a radio/checkbox button if the user clicks on the + // corresponding label. Because of this, if the user clicks on the label, + // an even is manually fired on the the radio button. This only applies + // if the user clicks on the actual label, not on any input elements + // contained within the label. This restriction is necessary or else we + // would be in a never ending loop that would continually toggle the state + // of any check boxes. + if(dom.is(event.target, "label")) { + // Must prevent standard acting browsers from taking care of the click or + // else it acts like two consecutive clicks. For radio buttons this will + // just toggle state. + event.preventDefault(); + + var target = dom.getAttr(event.target, "for"); + dom.fireEvent("#" + target, event.type); } } @@ -100,7 +112,7 @@ BrowserID.Modules.PickEmail = (function() { // The click function does not pass the event to the function. The event // is needed for the label handler so that the correct radio button is // selected. - self.bind("#selectEmail label", "click", selectEmail); + self.bind("#selectEmail label", "click", proxyEventToInput); sc.start.call(self, options); diff --git a/resources/static/dialog/resources/state.js b/resources/static/dialog/resources/state.js index 9af733f522347cc61b74b628f33538c0a57b2544..846eaddb7e0b4d48302872ca664419e9599bf30b 100644 --- a/resources/static/dialog/resources/state.js +++ b/resources/static/dialog/resources/state.js @@ -162,6 +162,8 @@ BrowserID.State = (function() { }); subscribe("email_chosen", function(msg, info) { + info = info || {}; + var email = info.email, idInfo = storage.getEmail(email); @@ -214,7 +216,7 @@ BrowserID.State = (function() { }); subscribe("authenticated", function(msg, info) { - publish("pick_email"); + publish("email_chosen", info); }); subscribe("forgot_password", function(msg, info) { diff --git a/resources/static/dialog/views/authenticate.ejs b/resources/static/dialog/views/authenticate.ejs index 7e08dedb9834d0eb6ae564abd7870b173404efe7..38292d152c943183a58aa74908d30a9b37853eea 100644 --- a/resources/static/dialog/views/authenticate.ejs +++ b/resources/static/dialog/views/authenticate.ejs @@ -32,7 +32,7 @@ <li id="create_text_section" class="newuser"> <p><strong><%= gettext('Welcome to BrowserID!') %></strong></p> - <p><%= gettext('This email looks new, so let's get you set up.') %></p> + <p><%= gettext("This email looks new, so let's get you set up.") %></p> </li> <li class="returning"> @@ -58,6 +58,6 @@ <button class="start" tabindex="3"><%= gettext('next') %></button> <button class="newuser" tabindex="3"><%= gettext('verify email') %></button> - <button class="returning" tabindex="3"><%= gettext('select email') %></button> + <button class="returning" tabindex="3"><%= gettext('sign in') %></button> </div> </div> diff --git a/resources/static/lib/dom-jquery.js b/resources/static/lib/dom-jquery.js index 889d165bf42ce1080d6326e6559431ba148a0d18..860c033277fa2579044a22216426b7ac88bbd8eb 100644 --- a/resources/static/lib/dom-jquery.js +++ b/resources/static/lib/dom-jquery.js @@ -296,6 +296,20 @@ BrowserID.DOM = ( function() { */ focus: function( elementToFocus ) { jQuery( elementToFocus ).focus(); + }, + + /** + * Check the current matched set of elements against + * a selector or element and return true if at least + * one of these elements matches the given arguments. + * @method is + * @param {selector || element} elementToCheck + * @param {string} type + * @returns {boolean} true if elementToCheck matches the specified + * type, false otw. + */ + is: function( elementToCheck, type ) { + return jQuery( elementToCheck ).is( type ); } diff --git a/resources/static/test/cases/controllers/authenticate.js.bak b/resources/static/test/cases/controllers/authenticate.js.bak deleted file mode 100644 index a77ba9c3d829b753c1bcce1feff8aa6e5801f5e5..0000000000000000000000000000000000000000 --- a/resources/static/test/cases/controllers/authenticate.js.bak +++ /dev/null @@ -1,257 +0,0 @@ -/*jshint browsers:true, forin: true, laxbreak: true */ -/*global test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -(function() { - "use strict"; - - var controller, - el = $("body"), - bid = BrowserID, - storage = bid.Storage, - network = bid.Network, - xhr = bid.Mocks.xhr, - emailRegistered = false, - userCreated = true, - mediator = bid.Mediator, - registrations = [], - testHelpers = bid.TestHelpers, - register = testHelpers.register, - provisioning = bid.Mocks.Provisioning; - - function reset() { - emailRegistered = false; - userCreated = true; - } - - function createController(options) { - options = options || {}; - controller = bid.Modules.Authenticate.create(); - controller.start(options); - } - - module("controllers/authenticate", { - setup: function() { - reset(); - testHelpers.setup(); - }, - - teardown: function() { - if (controller) { - try { - controller.destroy(); - } catch(e) { - // may already be destroyed from close inside of the controller. - } - } - reset(); - testHelpers.teardown(); - } - }); - - asyncTest("providing primary email address - only show email address", function() { - $("#email").val(""); - createController({ - email: "registered@testuser.com", - type: "primary", - ready: function() { - equal($("#email").val(), "registered@testuser.com", "email prefilled"); - equal(false, "need a test"); - start(); - } - }); - }); - - asyncTest("providing known secondary - show password", function() { - $("#email").val(""); - createController({ - email: "registered@testuser.com", - type: "secondary", - known: true, - ready: function() { - equal($("#email").val(), "registered@testuser.com", "email prefilled"); - equal(false, "need a test"); - start(); - } - }); - }); - - asyncTest("providing unknown secondary address - show email address, nothing more", function() { - $("#email").val(""); - createController({ - email: "unregistered@testuser.com", - type: "secondary", - known: false, - ready: function() { - equal($("#email").val(), "unregistered@testuser.com", "email prefilled"); - equal(false, "need a test"); - start(); - } - }); - }); - - function testUserUnregistered() { - register("create_user", function() { - ok(true, "email was valid, user not registered"); - start(); - }); - - controller.checkEmail(); - } - - function testUserUnregistered() { - var createUserCalled = false; - register("create_user", function() { - createUserCalled = true; - }); - - controller.checkEmail(function() { - equal(createUserCalled, true, "create_user was triggered"); - start(); - }); - } - - asyncTest("checkEmail with unknown secondary email, expect 'create_user' message", function() { - createController(); - $("#email").val("unregistered@testuser.com"); - xhr.useResult("unknown_secondary"); - - testUserUnregistered(); - }); - - asyncTest("checkEmail with email with leading/trailing whitespace, user not registered, expect 'create_user' message", function() { - createController(); - $("#email").val(" unregistered@testuser.com "); - xhr.useResult("unknown_secondary"); - - testUserUnregistered(); - }); - - asyncTest("checkEmail with normal email, user registered, expect 'enter_password' message", function() { - createController(); - $("#email").val("registered@testuser.com"); - xhr.useResult("known_secondary"); - - register("enter_password", function() { - ok(true, "email was valid, user registered"); - start(); - }); - - controller.checkEmail(); - }); - - asyncTest("checkEmail with email that has IdP support, expect 'primary_user' message", function() { - createController(); - $("#email").val("unregistered@testuser.com"); - xhr.useResult("primary"); - - register("primary_user", function(msg, info) { - equal(info.email, "unregistered@testuser.com", "email correctly passed"); - equal(info.auth, "https://auth_url", "IdP authentication URL passed"); - equal(info.prov, "https://prov_url", "IdP provisioning URL passed"); - start(); - }); - - controller.checkEmail(); - }); - - function testAuthenticated() { - register("authenticated", function() { - ok(true, "user authenticated as expected"); - start(); - }); - controller.authenticate(); - } - - asyncTest("normal authentication is kosher", function() { - createController(); - $("#email").val("registered@testuser.com"); - $("#password").val("password"); - - testAuthenticated(); - }); - - asyncTest("leading/trailing whitespace on the username is stripped for authentication", function() { - createController(); - $("#email").val(" registered@testuser.com "); - $("#password").val("password"); - - testAuthenticated(); - }); - - asyncTest("forgotPassword triggers forgot_password message", function() { - createController(); - $("#email").val("registered@testuser.com"); - - register("forgot_password", function(msg, info) { - equal(info.email, "registered@testuser.com", "forgot_password with correct email triggered"); - start(); - }); - - controller.forgotPassword(); - }); - - asyncTest("createUser with valid email", function() { - createController(); - $("#email").val("unregistered@testuser.com"); - xhr.useResult("unknown_secondary"); - - register("user_staged", function(msg, info) { - equal(info.email, "unregistered@testuser.com", "user_staged with correct email triggered"); - start(); - }); - - controller.createUser(); - }); - - asyncTest("createUser with invalid email", function() { - createController(); - $("#email").val("unregistered"); - - var handlerCalled = false; - register("user_staged", function(msg, info) { - handlerCalled = true; - }); - - controller.createUser(function() { - equal(handlerCalled, false, "bad jiji, user_staged should not have been called with invalid email"); - start(); - }); - }); - - asyncTest("createUser with valid email but throttling", function() { - createController(); - $("#email").val("unregistered@testuser.com"); - - var handlerCalled = false; - register("user_staged", function(msg, info) { - handlerCalled = true; - }); - - xhr.useResult("throttle"); - controller.createUser(function() { - equal(handlerCalled, false, "bad jiji, user_staged should not have been called with throttling"); - equal(bid.Tooltip.shown, true, "tooltip is shown"); - start(); - }); - }); - - asyncTest("createUser with valid email, XHR error", function() { - createController(); - $("#email").val("unregistered@testuser.com"); - - var handlerCalled = false; - register("user_staged", function(msg, info) { - handlerCalled = true; - }); - - xhr.useResult("ajaxError"); - controller.createUser(function() { - equal(handlerCalled, false, "bad jiji, user_staged should not have been called with XHR error"); - start(); - }); - }); - -}()); - diff --git a/resources/static/test/cases/controllers/pick_email.js b/resources/static/test/cases/controllers/pick_email.js index 749047ca42b1ceb76c71422acc2d38d58f529235..8a1309a3762046461dfcca4eae65132bcd713947 100644 --- a/resources/static/test/cases/controllers/pick_email.js +++ b/resources/static/test/cases/controllers/pick_email.js @@ -64,7 +64,7 @@ var radioButton = $("input[type=radio]").eq(0); ok(radioButton.is(":checked"), "the email address we specified is checked"); - var label = radioButton.parent(); + var label = $("label[for=" + radioButton.attr("id") + "]"); ok(label.hasClass("preselected"), "the label has the preselected class"); }); @@ -155,16 +155,46 @@ controller.addEmail(); }); - test("click on an email label - select corresponding radio button", function() { + test("click on an email label and radio button - select corresponding radio button", function() { storage.addEmail("testuser@testuser.com", {}); storage.addEmail("testuser2@testuser.com", {}); createController(false); equal($("#testuser_testuser_com").is(":checked"), false, "radio button is not selected before click."); - $("label[for=testuser_testuser_com]").trigger("click"); + // selects testuser@testuser.com + $("label[for=testuser_testuser_com]").trigger("click"); equal($("#testuser_testuser_com").is(":checked"), true, "radio button is correctly selected"); + + // selects testuser2@testuser.com + $("#testuser2_testuser_com").trigger("click"); + equal($("#testuser2_testuser_com").is(":checked"), true, "radio button is correctly selected"); + }); + + test("click on the 'Always sign in...' label and checkbox - correct toggling", function() { + createController(true); + + var label = $("label[for=remember]"), + checkbox = $("#remember").removeAttr("checked"); + + equal(checkbox.is(":checked"), false, "checkbox is not yet checked"); + + // toggle checkbox to on clicking on label + label.trigger("click"); + equal(checkbox.is(":checked"), true, "checkbox is correctly checked"); + + // toggle checkbox to off clicking on label + label.trigger("click"); + equal(checkbox.is(":checked"), false, "checkbox is correctly unchecked"); + + // toggle checkbox to on clicking on checkbox + checkbox.trigger("click"); + equal(checkbox.is(":checked"), true, "checkbox is correctly checked"); + + // toggle checkbox to off clicking on checkbox + checkbox.trigger("click"); + equal(checkbox.is(":checked"), false, "checkbox is correctly unchecked"); }); }()); diff --git a/resources/static/test/cases/pages/add_email_address_test.js b/resources/static/test/cases/pages/add_email_address_test.js index 1d3844b7cb82edec1435f4a9a0e6a30086b2257e..79fe35d5e22cb38c4888550cd30c91643f728d3d 100644 --- a/resources/static/test/cases/pages/add_email_address_test.js +++ b/resources/static/test/cases/pages/add_email_address_test.js @@ -133,10 +133,7 @@ }); asyncTest("password: too long of a password", function() { - var tooLong = ""; - for(var i = 0; i < 81; i++) { - tooLong += (i % 10); - } + var tooLong = testHelpers.generateString(81); $("#password").val(tooLong); $("#vpassword").val(tooLong); diff --git a/resources/static/test/cases/pages/manage_account.js b/resources/static/test/cases/pages/manage_account.js index f92596004f3fead7c1b5ad9be2d269a3dc9c97b0..5e1ddb2cca1febf5e0cf97fb6230366a44b176ab 100644 --- a/resources/static/test/cases/pages/manage_account.js +++ b/resources/static/test/cases/pages/manage_account.js @@ -201,14 +201,10 @@ asyncTest("changePassword with too long of a password - tooltip", function() { bid.manageAccount(mocks, function() { $("#old_password").val("oldpassword"); - var tooLong = ""; - for(var i = 0; i < 81; i++) { - tooLong += (i % 10); - } - $("#new_password").val(tooLong); + $("#new_password").val(testHelpers.generateString(81)); bid.manageAccount.changePassword(function(status) { - equal(status, false, "on too short of a password, status is false"); + equal(status, false, "on too long of a password, status is false"); testHelpers.testTooltipVisible(); start(); }); diff --git a/resources/static/test/cases/pages/verify_email_address_test.js b/resources/static/test/cases/pages/verify_email_address_test.js index a4c3ef06d7407dd377bcb79b1310da0c59424357..c11ae2574bd2b46e071d12037a981eed2ca1907c 100644 --- a/resources/static/test/cases/pages/verify_email_address_test.js +++ b/resources/static/test/cases/pages/verify_email_address_test.js @@ -11,6 +11,7 @@ storage = bid.Storage, xhr = bid.Mocks.xhr, testHelpers = bid.TestHelpers, + testTooltipVisible = testHelpers.testTooltipVisible, validToken = true; module("pages/verify_email_address", { @@ -82,6 +83,35 @@ bid.verifyEmailAddress.submit(function() { equal($("#congrats").is(":visible"), false, "congrats is not visible, missing password"); + testTooltipVisible(); + start(); + }); + }); + }); + + asyncTest("submit with good token, too short of a password", function() { + bid.verifyEmailAddress("token", function() { + var pass = testHelpers.generateString(6); + $("#password").val(pass); + $("#vpassword").val(pass); + + bid.verifyEmailAddress.submit(function() { + equal($("#congrats").is(":visible"), false, "congrats is not visible, too short of a password"); + testTooltipVisible(); + start(); + }); + }); + }); + + asyncTest("submit with good token, too long of a password", function() { + bid.verifyEmailAddress("token", function() { + var pass = testHelpers.generateString(81); + $("#password").val(pass); + $("#vpassword").val(pass); + + bid.verifyEmailAddress.submit(function() { + equal($("#congrats").is(":visible"), false, "congrats is not visible, too long of a password"); + testTooltipVisible(); start(); }); }); @@ -96,6 +126,7 @@ bid.verifyEmailAddress.submit(function() { equal($("#congrats").is(":visible"), false, "congrats is not visible, missing verification password"); + testTooltipVisible(); start(); }); @@ -109,6 +140,7 @@ bid.verifyEmailAddress.submit(function() { equal($("#congrats").is(":visible"), false, "congrats is not visible, different passwords"); + testTooltipVisible(); start(); }); diff --git a/resources/static/test/cases/resources/state.js b/resources/static/test/cases/resources/state.js index 53c9726695c97d05ee8c092d7e065f3ee2826020..9bdb1d44c3e94365d594914c3ce2a5b2b24bdb5f 100644 --- a/resources/static/test/cases/resources/state.js +++ b/resources/static/test/cases/resources/state.js @@ -170,10 +170,11 @@ ok(actions.called.doEmailChosen, "doEmailChosen called"); }); - test("authenticated", function() { - mediator.publish("authenticated"); + test("authenticated - call doEmailChosen", function() { + storage.addEmail("testuser@testuser.com", {}); + mediator.publish("authenticated", { email: "testuser@testuser.com" }); - ok(actions.called.doPickEmail, "doPickEmail has been called"); + ok(actions.called.doEmailChosen, "doEmailChosen has been called"); }); test("forgot_password", function() { diff --git a/resources/static/test/testHelpers/helpers.js b/resources/static/test/testHelpers/helpers.js index 35b33af5901a2a59f1d6a87390fa788444e227ea..493ea1db0756779b69735a7f18c104b51c1d3e26 100644 --- a/resources/static/test/testHelpers/helpers.js +++ b/resources/static/test/testHelpers/helpers.js @@ -180,6 +180,17 @@ BrowserID.TestHelpers = (function() { } cb && cb.apply(null, args); + }, + + /** + * Generate a long string + */ + generateString: function(length) { + var str = ""; + for(var i = 0; i < length; i++) { + str += (i % 10); + } + return str; } }; diff --git a/resources/views/add_email_address.ejs b/resources/views/add_email_address.ejs index 4b704d65ff66a3a4ad4846e6279f2a42fa389d71..979cea68aa1953636faecadd5c2370d81a366722 100644 --- a/resources/views/add_email_address.ejs +++ b/resources/views/add_email_address.ejs @@ -10,7 +10,7 @@ </ul> <form id="signUpForm" class="cf"> - <p class="hint siteinfo"><%= gettext('Finish signing into: ') %><strong><span class="website"></span></strong></p> + <p class="hint siteinfo"><%= gettext('Finish signing into:') %> <strong><span class="website"></span></strong></p> <h1 class="serif"><%= gettext('Email Verification') %></h1> diff --git a/resources/views/index.ejs b/resources/views/index.ejs index b07203f655059f705ee9ea99c8da847928294d35..57f1c8d7b96254292d735fb74bf6bab32639dd4a 100644 --- a/resources/views/index.ejs +++ b/resources/views/index.ejs @@ -52,6 +52,10 @@ </div> <div id="vAlign" class="display_nonauth"> + <div id="newsbanner"> + BrowserID is graduating: we're launching <b>Mozilla Persona</b>. Find out more on <a href="http://identity.mozilla.com/">the identity blog</a>. + </div> + <div id="signUp"> <div id="card"><img src="/i/slit.png"></div> <div id="hint"></div> diff --git a/resources/views/verify_email_address.ejs b/resources/views/verify_email_address.ejs index 3d776747f9385c9ce317af45c304a3e2c0dc9ea0..e86a1f5b6a41afa004449cae41925c9636ca6104 100644 --- a/resources/views/verify_email_address.ejs +++ b/resources/views/verify_email_address.ejs @@ -6,12 +6,12 @@ <div id="signUpFormWrap"> <ul class="notifications"> <li class="notification error" id="cannotconfirm"><%= gettext('There was a problem with your signup link. Has this address already been registered?') %></li> - <li class="notification error" id="cannotcommunicate"><%= gettext('Error comunicating with server.') %></li> + <li class="notification error" id="cannotcommunicate"><%= gettext('Error communicating with server.') %></li> <li class="notification error" id="cannotcomplete"><%= gettext('Error encountered trying to complete registration.') %></li> </ul> <form id="signUpForm" class="cf"> - <p class="hint siteinfo"><%= gettext('Finish signing into: ') %><strong><span class="website"></span></strong></p> + <p class="hint siteinfo"><%= gettext('Finish signing into:') %> <strong><span class="website"></span></strong></p> <h1 class="serif"><%= gettext('Last step!') %></h1> <ul class="inputs"> diff --git a/scripts/browserid.spec b/scripts/browserid.spec index 87bfbeb9e0f42d5f8414b4a18bea9b253ec0e04a..6ed39bbfd82a3d7c4c310e7b9d8de75e05742771 100644 --- a/scripts/browserid.spec +++ b/scripts/browserid.spec @@ -12,7 +12,7 @@ Source0: %{name}.tar.gz BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root AutoReqProv: no Requires: openssl nodejs -BuildRequires: gcc-c++ git jre make npm openssl-devel expat-devel perl perl-JSON perl-Locale-PO +BuildRequires: gcc-c++ git jre make npm openssl-devel expat-devel %description browserid server & web home for browserid.org @@ -37,6 +37,8 @@ mkdir -p %{buildroot}%{_rootdir} for f in bin lib locale node_modules resources scripts *.json; do cp -rp $f %{buildroot}%{_rootdir}/ done +mkdir -p %{buildroot}%{_rootdir}/config +cp -p config/l10n-all.json %{buildroot}%{_rootdir}/config %clean rm -rf %{buildroot} diff --git a/tests/page-requests-test.js b/tests/page-requests-test.js index b4574b1125853925665b0ea6e788447aa3d895ab..0ee2537c058c543df04c5aabcc707c40fbda5a23 100755 --- a/tests/page-requests-test.js +++ b/tests/page-requests-test.js @@ -76,7 +76,7 @@ suite.addBatch({ 'GET /primaries/': respondsWith(302), 'GET /developers': respondsWith(302), 'GET /developers/': respondsWith(302), - 'GET /test': respondsWith(200), + 'GET /test': respondsWith(301), 'GET /test/': respondsWith(200), 'GET /include.js': respondsWith(200), 'GET /include.orig.js': respondsWith(200)