Test utilities for repoze.who-powered applications

Author:Gustavo Narea.
Latest release:1.0.1

Overview

repoze.who-testutil is a repoze.who plugin which modifies repoze.who‘s original middleware to make it easier to forge authentication, without bypassing identification (this is, running the metadata providers).

It’s been created in order to ease testing of repoze.who-powered applications, in a way independent of the identifiers, authenticators and challengers used originally by your application, so that you won’t have to update your test suite as your application grows and the authentication method changes.

The problems

While testing protected areas, you have to authenticate first

And that’s absolutely specific to the identifiers/challengers you’re using.

For example, if you’re using WebTest and the repoze.who.plugins.form.RedirectingFormPlugin plugin, you have to get the login handler at the beginning of each test that covers situations where the user is authenticated:

class TestControlPanel(TestCase):

    def setUp(self):
        from paste.deploy import loadapp
        from webtest import TestApp
        wsgiapp = loadapp('config:test.ini')
        self.app = TestApp(wsgiapp)

    def test_index_as_admin(self):
        # First of all, let's log in the RedirectingFormPlugin way:
        self.app.get('/login_handler?login=admin&password=somepass')
        # Now that we're authenticated, let's request the control panel:
        resp = self.app.get('/panel/', status=200)
        assert "some text" in resp.body

    def test_index_as_normal_user(self):
        # First of all, let's log in the RedirectingFormPlugin way:
        self.app.get('/login_handler?login=foo&password=bar')
        # Now that we're authenticated, let's request the control panel:
        self.app.get('/panel/', status=302)
        # We got a 302 redirection. This is the RedirectingFormPlugin way
        # to let us know that authorization was denied.

    def test_index_as_anonymous(self):
        # Let's request the control panel as an anonymous user:
        self.app.get('/panel/', status=302)
        # We got a 302 redirection. This is the RedirectingFormPlugin way
        # to let us know that authorization was denied.

This is, we end up testing protected areas in a way that is totally tied to the repoze.who identifiers and challengers you intend to use initially. If the are replaced later, you will have to update many of your tests (most of them, possibly).

Or, while testing protected areas, you have to forge authentication

But that will bypass identification: The metadata providers won’t get run, so this is not an option if your application relies on them:

class TestControlPanel(TestCase):

    def setUp(self):
        from paste.deploy import loadapp
        from webtest import TestApp
        wsgiapp = loadapp('config:test.ini')
        self.app = TestApp(wsgiapp)

    def test_index_as_admin(self):
        # Let's forge authentication:
        environ = {'REMOTE_USER': 'admin'}
        resp = self.app.get('/panel/', extra_environ=environ, status=200)
        assert "some text" in resp.body

This seems like the best way to test a protected area, and you may expect it to work, but unfortunately it won’t:

If the controller action for '/panel/' assumes that if the user is authenticated, her full name will be available in identity['full_name'], then your test will be broken because such an item won’t be defined in the identity dict – even worse: the identity dict won’t even be defined because repoze.who will assume that, because environ['REMOTE_USER'] is set, it won’t be necessary to run its middleware.

The solution

It’s absolutely unnecessary to test authentication every time you test a protected area in your Web site; authentication should be tested once and separately. This is, to test a protected area in a Web site, only identification and authorization are required, not authentication.

With repoze.who-testutil, you’ll be able to write tests for protected areas the way you’d expect:

class TestControlPanel(TestCase):

    def setUp(self):
        from paste.deploy import loadapp
        from webtest import TestApp
        wsgiapp = loadapp('config:test.ini')
        self.app = TestApp(wsgiapp)

    def test_index_as_admin(self):
        environ = {'REMOTE_USER': 'admin'}
        resp = self.app.get('/panel/', extra_environ=environ, status=200)
        assert "some text" in resp.body

    def test_index_as_normal_user(self):
        environ = {'REMOTE_USER': 'foobar'}
        # The 403 HTTP status code means that authorization has been
        # denied, while we are aware of who the user is:
        self.app.get('/panel/', status=403)

    def test_index_as_anonymous(self):
        # Let's request the control panel as an anonymous user.
        # The 401 HTTP status code means that authorization has been
        # denied, although it may be granted if the user logs in:
        self.app.get('/panel/', extra_environ=environ, status=401)

As you may have noticed, these tests are absolutely independent of the repoze.who plugins used. And the best of all: The repoze.who middleware won’t be skipped, so the identity dict will be defined as usual!

Then if you want to test authentication, you can do it once and separately – and if the authentication method changes over time, you’d just have to update a few tests, instead of all the tests that cover protected areas.

How to install

It requires repoze.who only, and you can install them with easy_install:

easy_install repoze.who-testutil

Support and development

The prefered place to ask questions is the Repoze mailing list or the #repoze IRC channel. Bugs reports and feature requests should be sent to the issue tracker of the Repoze project.

The development mainline is available at the following Subversion repository:

http://svn.repoze.org/whoplugins/whotestutil/trunk/