Apr 24 2012

Using SpecsFor.Mvc - Dealing with Authentication

Category: SpecsFor | TestingMatt @ 15:31

In all of the SpecsFor.Mvc examples I’ve posted so far, I’ve omitted one common cross-cutting requirement of web applications: authentication!  Most web apps have some sort of authentication, and we need to be able to test our core application logic without this cross-cutting concern getting in the way.  SpecsFor.Mvc makes it easy to achieve exactly that, as I’ll show you in this post.

The “Using SpecsFor.Mvc” Series

Our Configuration

Once again we’ll be using the SpecsFor.Demo application (available on Github) with a standard SpecsFor.Mvc configuration:

[SetUpFixture]
public class AssemblyStartup
{
    private SpecsForIntegrationHost _host;

    [SetUp]
    public void SetupTestRun()
    {
        var config = new SpecsForMvcConfig();
        config.UseIISExpress()
            .With(Project.Named("SpecsFor.Mvc.Demo"))
            .ApplyWebConfigTransformForConfig("Test");

        config.BuildRoutesUsing(r => MvcApplication.RegisterRoutes(r));
        config.UseBrowser(BrowserDriver.InternetExplorer);

        config.InterceptEmailMessagesOnPort(13565);

        _host = new SpecsForIntegrationHost(config);
        _host.Start();
    }

    [TearDown]
    public void TearDownTestRun()
    {
        _host.Shutdown();
    }
}

Check out part one if you want to know more about what this configuration is doing.

Authentication the Wrong Way

One way to handle authentication with SpecsFor.Mvc is to log in at the beginning of each spec.  Here’s how that might look using SpecsFor:

public class when_accessing_some_protected_resource : SpecsFor<MvcWebApp>
{
    protected override void Given()
    {
        //First we need to log in...
        SUT.NavigateTo<AccountController>(c => c.LogOn());
        SUT.FindFormFor<LogOnModel>()
            .Field(m => m.UserName).SetValueTo("real@user.com")
            .Field(m => m.Password).SetValueTo("RealPassword")
            .Submit();

        //Then we can navigate to the target resource...
        SUT.NavigateTo<SomeProtectedController>(c => c.Private());
    }

    ...
}

This approach is going to lead to a lot of duplicated code, but even worse, it’s going to obscure the true intent of the spec.  You could move the logic to a base test class:

public abstract class given_we_have_authenticated : SpecsFor<MvcWebApp>
{
    protected override void  Given()
    {
        SUT.NavigateTo<AccountController>(c => c.LogOn());
        SUT.FindFormFor<LogOnModel>()
            .Field(m => m.UserName).SetValueTo("real@user.com")
            .Field(m => m.Password).SetValueTo("RealPassword")
            .Submit();
    }
}

public class when_accessing_some_protected_resource : given_we_have_authenticated
{
    protected override void Given()
    {
        base.Given();

        SUT.NavigateTo<SomeProtectedController>(c => c.Private());
    }

    ...
}

That introduces another inheritance requirement though and still makes our specs more complicated than they need to be.  Let’s look at the right way to tackle this.

Authentication the SpecsFor.Mvc Way

SpecsFor.Mvc includes a simple way to handle authentication.  During configuration, you can specify a handler that will be used to authenticate with the application at the beginning of each spec.  Let’s update the SpecsForMvcConfig in our AssemblyStartup to specify an authentication handler:

public class AssemblyStartup
{
    private SpecsForIntegrationHost _host;

    [SetUp]
    public void SetupTestRun()
    {
        var config = new SpecsForMvcConfig();
        config.UseIISExpress()
            .With(Project.Named("SpecsFor.Mvc.Demo"))
            .ApplyWebConfigTransformForConfig("Test");

        config.BuildRoutesUsing(r => MvcApplication.RegisterRoutes(r));
        config.RegisterArea<TasksAreaRegistration>();

        config.UseBrowser(BrowserDriver.InternetExplorer);

        config.InterceptEmailMessagesOnPort(13565);

        config.AuthenticateBeforeEachTestUsing<StandardAuthenticator>();

        _host = new SpecsForIntegrationHost(config);
        _host.Start();
    }

    [TearDown]
    public void TearDownTestRun()
    {
        _host.Shutdown();
    }
}

The implementation of the handler is simple.  It only needs to navigate to the login form, populate it with valid credentials, and submit it:

public class StandardAuthenticator : IHandleAuthentication
{
    public void Authenticate(MvcWebApp mvcWebApp)
    {
        mvcWebApp.NavigateTo<AccountController>(c => c.LogOn());

        mvcWebApp.FindFormFor<LogOnModel>()
            .Field(m => m.UserName).SetValueTo("real@user.com")
            .Field(m => m.Password).SetValueTo("RealPassword")
            .Submit();
    }
}

Now we can write specs against controllers that are not accessible to unauthenticated users without authentication concerns obscuring our specs. 

What’s Next?

You now know how to create and configure a handler for authentication, but how do you setup the credentials to use for authentication?  You could manually configure the database your app uses during integration testing, but SpecsFor.Mvc actually provides another solution.  In the next post, we’ll look at how you can load seed data into your application for use during your integration tests.  In the meantime, the package is on NuGet, and the code is on Github.  Enjoy!

Tags:

blog comments powered by Disqus