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.