Sunday, May 26, 2013

RSpec / Devise / Capybara & feature specs with HTTP Authentication

I'm working on a couple of sites which use HTTP Authentication and - after much googling and failed attempts - developed a way to deal with that with a set of spec helpers. It all started with this gist from Matt Connolly - thanks Matt!

For the record, I'm developing with the following:
ruby 1.9.3p327
rails 3.2.12
devise  2.2.3
capybara 1.1.4
rspec  2.13.0

What I wanted was to be able to say:
def before do
  login_user()
end
and have it Just Work.

In preparation for Capybara 2.0, I'm putting all my integration tests in the spec/features directory and that created some of the confusion. Code in the spec/integration directory has access to the controller, whereas code in the spec/features directory does not. This means that both HTTP Authentication and Devise login must be handled differently.  To resolve this, I started with Matt's approach and modified it so that (so far), it works in any of my tests.

First, I modified Matt's module to add login processing and separate out the HTTP Authentication so that it could be used for controller as well as feature specs. Here's the code:

## spec/support/auth_helper.rb
module HTTPHelper
  def http_config(test_type)
    @test_type = test_type
  end

  def http_login(user,pw)
    puts "@test_type: #{@test_type}"
    if @test_type == :controller then
      request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user,pw)
    elsif @test_type == :feature then
      if page.driver.respond_to?(:basic_auth)
        puts 'Responds to basic_auth'
        page.driver.basic_auth(user, pw)
      elsif page.driver.respond_to?(:basic_authorize)
        puts 'Responds to basic_authorize'
        page.driver.basic_authorize(user, pw)
      elsif page.driver.respond_to?(:browser) && page.driver.browser.respond_to?(:basic_authorize)
        puts 'Responds to browser_basic_authorize'
        page.driver.browser.basic_authorize(user, pw)
      elsif page.driver.respond_to?(:browser) && page.driver.respond_to?(:header)
        encoded_login = ["#{user}:#{pw}"].pack("m*")
        page.driver.header 'Authorization', "Basic #{encoded_login}"
      else
        puts "page.driver.methods: #{page.driver.methods.sort}"
        if page.driver.respond_to?(:browser) then
          puts "page.driver.browser methods: #{page.driver.browser.methods.sort}"
        end
        raise "I don't know how to log in!"
      end
    else
      raise "I don't know what kind of test this is!"
    end
  end  
end

module AuthHelper
  include HTTPHelper
### For controller specs

  def login_admin
    login_user(:admin)
  end
  def login_user(user_name=nil)
    http_config :controller
    http_login('HTTPname', 'HTTPpassword')
    if user_name.nil? then
      @current_user = FactoryGirl.create :user
      @current_user.confirm!
      sign_in @current_user
    else
      raise NotImplementedError
      @current_user = FactoryGirl.create :user, :name => :user_name
      user = User.where(:name => user_name.to_s).first if user.is_a?(Symbol)
      sign_in user.id
    end
  end
  def current_login
    User.find(session[:user_id])
  end
end

module AuthRequestHelper
  include HTTPHelper
### For request, feature & view specs  
# pass the @env along with your request, eg:
# GET '/labels', {}, @env

  def login_user(user_name=nil)
    http_config :feature
    http_login('HTTPname','HTTPpassword')
    if user_name.nil? then
      @current_user = FactoryGirl.create :user
      @current_user.confirm!
      #Following does not work in feature specs
      #sign_in @current_user
      visit ('/')
      click_on 'Login'
      fill_in 'Email', with: @current_user.email
      fill_in 'Password', with: 'password'
      click_on 'Sign in'
    else
      raise NotImplementedError
    end
  end
end

## Relevant portion of spec/spec_helper.rb
...
  config.include HTTPHelper
  config.include AuthRequestHelper, :type => :request
  config.include AuthRequestHelper, :type => :feature
  config.include AuthRequestHelper, :type => :view
  config.include AuthHelper,        :type => :controller
...

## spec/support/devise.rb
RSpec.configure do |config|
  config.include Devise::TestHelpers, :type => :controller
  config.include Devise::TestHelpers, :type => :view
end

Couple things to note: BothHTTP Authentication and Devise sign_in are  different between controller and feature specs primarily, I believe, because of Capybara handling in the feature specs (remember, the controller isn't available in feature specs with Capybara see the "Gotchas" near the bottom of the Capybara Read Me). Also check out this useful post from Thoughtbot.

Before you say anything... yes, I know it's not very DRY: I could have just included the HTTP Authentication code in each of the different authorization modules. But while working on it, I wanted the code isolated for clarity and I'm just happy to have the thing working. Feel free to clean this up if you want.

For now I'm not using the login_admin method, but will be working with that down the line.

Now I can finally get back to the real work of developing my app; hope this helps someone else.

EDIT: Thanks to the folks at thoughbot for this link which describes how to login using Warden. I haven't tried it, but it looks promising.


No comments:

Post a Comment