Make Authlogic and Cucumber Play Nice

May 20th, 2011

I used to use Authlogic for just about every new Rails project. These days, I much prefer OAuth-style authentication using something like OmniAuth whenever possible. But sometimes, an application still needs its own internal authentication and in those cases, Authlogic is still my first choice. But after the last couple days of hair-pulling, I was this close to ditching Authlogic altogether and rolling my own.

It was a dark and stormy night…

Who knows when you’ll be reading this, so here’s a little background courtesy of my Gemfile:

source "http://rubygems.org"

gem "rails", "3.0.7"

gem "pg"

gem "hoptoad_notifier", "~> 2.4.9"
gem "god", "~> 0.11.0"
gem "resque", "~> 1.16.0"
gem "authlogic", "~> 3.0.3"

group :development, :test do
  gem "ruby-debug19", :require => "ruby-debug"
  gem "cucumber-rails", "~> 0.4.1"
  gem "capybara", "~> 0.4.1"
  gem "database_cleaner", "~> 0.6.7"
  gem "factory_girl_rails", "~> 1.0.1"
  gem "rspec-rails", "~> 2.6.0"
  gem "mocha", "~> 0.9.12"
  gem "webmock", "~> 1.6.2", :require => false
  gem "email_spec", "~> 1.1.1"
  gem "launchy", "~> 0.4.0"
  gem "timecop", "~> 0.3.5"
  gem "capistrano", "~> 2.6.0"
end

I’m writing up a simple Cucumber feature to ensure a user can log in if logged out, and… log out if logged in. Simple enough. Here’s the feature:

Feature: Authentication
  In order to manage my account
  As a registered user
  I want to log in and out

  Background:
    Given the following user exists:
      | email                   | password |
      | steve.richert@gmail.com | SECRET   |

  Scenario: Successful login
    Given I am not logged in
    And I am on the homepage
    When I follow "Log in"
    And I fill in the following:
      | Email address | steve.richert@gmail.com |
      | Password      | SECRET                  |
    And I press "Continue"
    Then I should be on the user page
    And I should see "Log out"
    And I should not see "Log in"

  Scenario: Successful logout
    Given I am logged in as "steve.richert@gmail.com"
    And I am on the user page
    When I follow "Log out"
    Then I should be on the homepage
    And I should see "Log in"
    And I should not see "Log out"

Pretty standard. I’m using the usual steps defined by Factory Girl and Capybara, except for two custom steps:

Given "I am not logged in" do
  UserSession.find.try(:destroy)
end

Given /^I am logged in as "([^"]*)"$/ do |email|
  UserSession.create!(User.find_by_email!(email))
end

As is often the case, everything is right in the world until I run the tests. In big bold red:

You must activate the Authlogic::Session::Base.controller with a controller object before creating objects (Authlogic::Session::Activation::NotActivatedError)

The problem is that the UserSession class isn’t “activated” until the application receives a request. At that time a before_filter sets UserSession.controller to the current controller instance and gives the session all of its magical access to the cookies.

After hours of scouring the Googles, I found a couple different techniques for dealing with this issue. Most popular by far was to change my nice one-liner steps to something like:

Given "I am not logged in" do
  Given %(I am on the homepage)
  And %(I follow "Log out")
end

Given /^I am logged in as "([^"]*)" with password  "([^"]*)"$/ do |email, password|
  Given %(I am on the login page)
  And %(I fill in "Email address" with "#{email}")
  And %(I fill in "Password" with "#{password}")
  And %(I press "Continue")
end

This works because I’m making requests to the application and activating that UserSession class.

And at the peak of my frustration, I actually considered this approach but I couldn’t bring myself to do it. Why? Because these steps are givens. A given sets the stage. It initializes the state of the application for when the real magic (whens and thens) happen. So why is my user jumping through all these hoops to set the stage? Why can’t my application do that itself?

The second approach looked much more promising. Authlogic has some test-friendliness baked right in. Unfortunately it seemed a bit Test::Unit and RSpec oriented, but it was worth a try! I put the following code in features/support/authlogic.rb:

require "authlogic/test_case"

World(Authlogic::TestCase)

Before do
  activate_authlogic
end

Very clean. I can keep my nice authentication steps. And this time, the cukes actually run! But… they fail. The application logs me in and redirects me as expected but it bombs afterward with:

undefined method `name' for nil:NilClass (ActionView::Template::Error)

It seems my current_user is missing, even after login. Argh!

If this sounds like anything you’ve been experiencing, I won’t keep you in suspense. Change your features/support/authlogic.rb to this…

require "authlogic/test_case"

World(Authlogic::TestCase)

ApplicationController.skip_before_filter :activate_authlogic

Before do
  activate_authlogic
end

…and be on your way.

Or if you’re interested in the explanation…

Authlogic inside of Cucumber is bipolar when you use Authlogic::TestCase. Inside the cukes, Authlogic is activated using this whole network of mocked controllers and mock requests and mock cookies. UserSession.find looks in those mock cookies. UserSession.create does likewise.

Meanwhile, your actual application code is clobbering it all with every request. Because Cucumber runs your full Rails stack, every real request to your controllers is firing the activate_authlogic before_filter, taking all that mocked out craziness in UserSession.controller and obliterating it with your actual application controller instance. Authlogic happily does its thing, both in the cukes and in your app. It just reads/writes from/to completely different cookie jars.

I tried to make Authlogic use the actual application cookies from inside my Cucumber suite, but it wasn’t worth the headache. So the answer I (finally) found was to add the following (as seen above):

ApplicationController.skip_before_filter :activate_authlogic

All your cukes will now use Authlogic’s test case mockery madness. And everything should just… work.

I hope it helps, and let me know if you find a way to avoid Authlogic::TestCase altogether!

blog comments powered by Disqus