Mocking Persona (browserid) in node using zombie.js

I wanted to be able to run headless integration tests in a nodejs app that require users to log in.  I’m using Mozilla Persona for authentication, which means that a full login and authorization workflow is a little complex:

flow

Given the heavy reliance on client-side javascript to request the assertion, request verification, and then respond to the application server’s authorization, it’s hard to just declare that “Jane is logged in” at the start of a test.  There’s a lot of state in there that needs to be set up.  We could write tests to use live persona servers (either using Mozilla’s, or downloading and running your own for offline testing), but this is heavy-weight and slow.  It can take several seconds for the whole chain to complete, and for headless integration tests that we want to incorporate into our development toolchains, it’s a bit much.

A better approach for my use case is to mock the persona requests and responses, and to stub them out of the flow.  The client side code still makes the same calls – firing navigator.id.request and naviator.id.watch – but we fake the requests and responses. I did this using ZombieJS, a headless browser emulator for Node.

1. Mock include.js

The first step is to mock the Mozilla Persona shim, include.js, which sets up navigator.id. We can replace it with an extremely simple shim that cuts out all the actual network traffic:

// include.js replacement:
var handlers = {};
navigator.id = {
    watch: function(obj) { handlers = obj; },
    request: function() { handlers.onlogin("faux-assertion"); },
    logout: function() { handlers.onlogout(); }
};

To get this stub to replace Mozilla’s include.js, we use the undocumented but very useful browser.resources.mock function in ZombieJS:

// Set up Zombie browser for tests
var Browser = require("zombie");
var browser = new Browser();
browser.resources.mock("https://login.persona.org/include.js", {
    statusCode: 200,
    headers: { "Content-Type": "text/javascript" },
    body: "var handlers = {};" +
      "navigator.id = { " +
      "  request: function() { handlers.onlogin("faux-assertion"); }," +
      "  watch: function(obj) { handlers = obj; }," +
      "  logout: function() { handlers.onlogout(); }" +
      "};"
});

This will short-circuit any attempt for a page to load https://login.persona.org/include.js, and will deliver the response we’ve provided instead.

2. Mock the server verification

I use my own node-browserid-consumer for my verification function, but the same principle should apply to whatever you’re using. The goal: have the test runner swap out the verification logic with something that just returns the address you expect.

var browserid = require("browserid-consumer");
browserid.verify = function(assertion, callback) {
    callback(null, {
        status: "okay",
        email: "test@mock",
        audience: "http://localhost:9000",
        expires: new Date().getTime() + 60*60*1000,
        issuer: "mock-stub"
    });
}

Replace test@mock with whatever email address you want to authenticate. And with that, we should be ready to go.

All together now

Now we’re ready to sign someone in rapidly in a test:

var Browser = require("zombie");
// Set up Zombie browser for tests
var browser = new Browser();
browser.resources.mock("https://login.persona.org/include.js", {
    statusCode: 200,
    headers: { "Content-Type": "text/javascript" },
    body: "var handlers = {};" +
      "navigator.id = { " +
      "  request: function() { handlers.onlogin("faux-assertion"); }," +
      "  watch: function(obj) { handlers = obj; }," +
      "  logout: function() { handlers.onlogout(); }" +
      "};"
});
var browserid = require("browserid-consumer");
browserid.verify = function(assertion, callback) {
    callback(null, {
        status: "okay",
        email: "test@mockmyid.com",
        audience: "http://localhost:9000",
        expires: new Date().getTime() + 60*60*1000,
        issuer: "mock-stub"
    });
}

// Now log the user in.
browser.visit("http://localhost:9000/");
browser.evaluate("$('signin').click();");

// .. a few milliseconds later.. you're authenticated!

To wait for auth to complete, I use a simple “await” function that just polls the browser to see if the user it’s done:

function awaitLogin(browser, callback) {
    // Replace this conditional with whatever you need to do to
    // see that a user is logged in.
    if (browser.evaluate("window.user")) {
       callback();
       return;
    }
    setTimeout(function() { awaitLogin(browser, callback) }, 100);
}

// Wait for login, then continue testing:
awaitLogin(browser, function() {
    // more tests here, with browser now logged in as 
    // test@mockmyid.com
});
Advertisements
This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s