Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails - Losing session with Integration Tests and Capybara - CSRF related?

I'm using Rails 3.1.0.rc4 and I'm working on doing integration tests with capybara's new Steak-like DSL and Rspec (using Devise authentication)

The issue I'm having is that when I run an integration test, the rack-test driver from capybara seems to just completely lose the user's logged in session, in fact, the session seems to just clear out altogether.

After days of debugging, I'm at a complete loss as to why. Going line by line through the middleware stack, I believe I've ruled the problem down to something going on in the ActiveRecord::SessionStore that is causing this. I've read here that Rails will clear out a session if it can't validate the CSRF token, which leaves me to believe that I've got something configured wrong, and for some reason this one test is not authenticating the CSRF token correctly.

This is what is in my session_store.rb in the /initializers directory:

MyApp::Application.config.session_store :active_record_store

Does anyone who knows about CSRF protection in rails have any leads on why this may be happening?

Also, here are some things to note:

  • the thing I'm trying to test actually works within the browser itself, only this one test is dropping the session
  • the session seems to get dropped after the submission of a form to which the action url is to another server. I'm using the VCR gem for capturing the requests/responses to this external server in the test, and while I believe I've ruled the external request as the problem, this may have something directly to do with the CSRF token not authenticating, thus clearing out the session.
  • other tests involving logging in / using sessions are not dropping sessions

Can anyone give me any leads as to what is going on here exactly, and why the one test just seems to arbitrarily drop its session and fail on me? I've done lots of debugging and have tried everything I can possible think of.

like image 508
joeellis Avatar asked Jul 07 '11 03:07

joeellis


4 Answers

I'm new to capybara too and I was having a similar problem.

I was trying to login a user doing something like this:

post user_session_path, :user => {:email => user.email, :password => 'superpassword'}

And that was working ok until I tried to do something with capybara, such as visiting a page and just testing if the user was logged in. This simple test was not passing:

visit root_path
page.should have_content("logout") #if the user is logged in then the logout link should be present

At first I thought capybara was clearing the sessions but I was wrong. The thing that took me some time to realize is that the driver capybara is using handles its own sessions, so, from the point of view of capybara my user was never logged in. To do so you have to do it like this

page.driver.post user_session_path, :user => {:email => user.email, :password => 'superpassword'}

Not sure if this is your case, but hope that helps.

like image 183
ariera Avatar answered Oct 27 '22 02:10

ariera


I was able to fix this error by setting this value to true in config/initializers/test.rb

# Disable request forgery protection in test environment
config.action_controller.allow_forgery_protection = true

Beforehand, the CSRF <meta> tags were not printing out to the <head>. After changing this value they finally appear.

like image 28
Adam Grant Avatar answered Oct 27 '22 04:10

Adam Grant


The manual way of doing it is very simple:

it "does something after login" do
  password = "secretpass"
  user = Factory(:user, :password => password)
  visit login_path
  fill_in "Email", :with => user.email
  fill_in "Password", :with => password
  click_button "Log in"
  visit # somewhere else and do something
end

You can then break this out into a function in your 'spec_helper.rb':

# at the bottom of 'spec_helper.rb'
def make_user_and_login
  password = "secretpass"
  @user = Factory(:user, :password => password)
  visit login_path
  fill_in "Email", :with => @user.email
  fill_in "Password", :with => password
  click_button "Log in"
end

and use it in any of your tests (probably request specs):

it "does something after login" do
  make_user_and_login
  # now test something that requires a logged in user
  # you have access to the @user instance variable
end
like image 45
brittohalloran Avatar answered Oct 27 '22 04:10

brittohalloran


This might be a long shot but I believe we end up in a bad state after click_button 'Sign in' and calling visit elsewhere immediately after.

My theory is that when we click the button the request hasn't completed yet, and we kill it by visiting another path.

From the Capybara documentation:

When issuing instructions to the DSL such as:

click_link('foo') click_link('bar') expect(page).to have_content('baz')

If clicking on the foo link triggers an asynchronous process, such as an Ajax request, which, when complete will add the bar link to the page, clicking on the bar link would be expected to fail, since that link doesn't exist yet. However Capybara is smart enough to retry finding the link for a brief period of time before giving up and throwing an error.

If this is the case the solution is simple: give Capybara something to look for and let it wait until the request is complete. This can be as simple as adding:

expect(page).to have_text('Signed in as [email protected]')

like image 41
jassa Avatar answered Oct 27 '22 04:10

jassa