Testing cookies in Capybara, Rack::Test, RSpec, and Rails: debugging other people's code (1 of 2)

Sunday 11 December 2011 at 23:39

This morning Sarah and the kids made gingerbread cookies, thoroughly destroying all illusions of joyous family fun for the holidays. Speaking of cookies failing to deliver promises, I blew the entire day Friday figuring out a failing capybara test around browser cookies. Not a good weekend for cookies.

It's a Rails app which depends entirely on a legacy authentication and authorization system. The simplest thing that could possibly work is use cookies set by the existing authentication system. Should be simple.

describe "ServiceRequestController" do
  it "redirects when credentials are missing" do
    get new_service_request_path
    response.status.should == 302
  end
end

Test is red.

class ServiceRequestController < ApplicationController
  def new
    redirect_to cookies_path
  end
end

Test is green.

describe "ServiceRequestController" do
  ...
  it "renders the form when credentials are found" do
    page.driver.browser.set_cookie 'username=jhendrix'
    get new_service_request_path
    response.status.should == 200
  end    
end

Test is red.

class ServiceRequestController < ApplicationController
  def new
    user = LegacyUser.find_by_username(request.cookies['username'])
    if !user
      redirect_to cookies_path
    end
  end
end

Test is red. I found a work-around just before lunch, but spent the rest of the day trying to understand why that doesn't work.

tl;dr

If you're here just for the work-around when testing with cookies in Capybara and RSpec, here's my first work around (but the better way to test cookies in Capybara, Rack::Test and RSpec is in part 2 ):

describe "ServiceRequestController" do
  ...
  it "renders the form when credentials are found" do
    page.driver.browser.set_cookie 'username=jhendrix'
    page.driver.get new_service_request_path
    response.status.should == 200
  end    
end

Use the page.driver when you call get.

Debugging other people's code: aka. code caving in a maze of twisty passages

What follows are some of the most important programming skills I learned working with Rob and Paul at bivio: how to find your way around other people's code. I'll start with the failing test.

describe "ServiceRequestController" do
  ...
  it "renders form when credentials are found" do
    page.driver.browser.set_cookie 'username=jhendrix'
    get new_service_request_path
    response.status.should == 200
  end    
end

First question: Am I calling set_cookie correctly? There's a profoundly important tone in this question. Start with a beginner's mind. Don't guess. Don't be clever. Just read and follow the code. This is also one of the hardest things to do. As programmers we're all pretty intoxicated with our own cleverness. Let go and use The Source.

In RubyMine you can Command-Click on set_cookie or type Command-B. That pulls up a list of likely candidates. Without RubyMine you need some unix fu. Two of the most important, general purpose, and language independent programming tools ever are find and grep. Get to know them. They are your best friends.

find ${GEM_PATH//:/ /} -type f -exec grep -nH -e 'def set_cookie' {} /dev/null \;

Either way you search, the results are similar (though not identical). These are cleaned up from the results via find and grep:


action_dispatch/http/response.rb:158:                   def set_cookie(key, value)
action_dispatch/middleware/session/cookie_store.rb:65:  def set_cookie(env, session_id, cookie)
rack/response.rb:58:                                    def set_cookie(key, value)
rack/session/abstract/id.rb:320:                        def set_cookie(env, headers, cookie)
rack/session/cookie.rb:121:                             def set_cookie(env, headers, cookie)
rack/mock_session.rb:23:                                def set_cookie(cookie, uri = nil)

The hits in action_dispatch, rack/response and rack/session are probably all related to the real cookie code. That stuff has to be working correctly or no Rails app would be able to save cookies. It almost has to be something in rack/mock_session.

There's a certain bit of irony here. I didn't trust these search results. I was Being Clever. I took a longer path and ended up in rack/mock_session anyway. The longer path did give me the confidence of a second opinion. Trusting The Source would have produced my answer more quickly. Then again, I might have found the answer and still not trusted it. I'll share the longer path I took which is pretty much what I could learn by stepping through with a debugger.

In this call, page.driver.browser.set_code where does page come from? I know it's from Capybara, but where exactly? It'll be in here somewhere: https://github.com/jnicklas/capybara/tree/master/lib/capybara.

cucumber.rb	
dsl.rb
rails.rb	
rspec.rb	
selector.rb	
server.rb	
session.rb	
version.rb
Looking over that list dsl.rb looked most promising. And sure enough in Capybara::DSL I find

def page
  Capybara.current_session
end

current_session is defined earlier in the file:

def current_session
  session_pool["#{current_driver}:#{session_name}:#{app.object_id}"] ||= Capybara::Session.new(current_driver, app)
end

Now I know that page is a Capybara::Session using current_driver. What's current_driver? It's also defined in dsl.rb:

def current_driver
  @current_driver || default_driver
end

And default_driver?

def default_driver
  @default_driver || :rack_test
end

So page is a Capybara::Session initalized with :rack_test. The relevant code:

attr_reader :mode, :app

def initialize(mode, app=nil)
  @mode = mode
  @app = app
end

def driver
  @driver ||= begin
    unless Capybara.drivers.has_key?(mode)
      other_drivers = Capybara.drivers.keys.map { |key| key.inspect }
      raise Capybara::DriverNotFoundError, "no driver called #{mode.inspect} was found, available drivers: #{other_drivers.join(', ')}"
    end
    Capybara.drivers[mode].call(app)
  end
end

Remind me again what it was I was searching for? I've bounced around the code enough to be lost in "a maze of twisty little passages, all alike." Oh yeah: page.driver.browser.set_cookie. After all this caving, I'm finally through page and now looking for driver. The good news is, it's right here: Capybara.drivers[:rack_test].call(app). But where do I find Capybara.drivers? Well given the name, let's look first at the top level capybara module: Capybara.

def drivers
  @drivers ||= {}
end

Searching through that file, this is the only line that refers to @drivers. What about references to drivers itself? Here's the only one I could find:

def register_driver(name, &block)
  drivers[name] = block
end

And register_driver?

Capybara.register_driver :rack_test do |app|
  Capybara::RackTest::Driver.new(app)
end

Now I know the driver is a Capybara::RackTest::Driver and the top of that file invites the next question: what's the browser?

  attr_reader :app, :options

def initialize(app, options={})
  raise ArgumentError, "rack-test requires a rack application, but none was given" unless app
  @app = app
  @options = DEFAULT_OPTIONS.merge(options)
end

def browser
  @browser ||= Capybara::RackTest::Browser.new(self)
end

Are we there yet? What was I looking for again? Oh, right. It's set_cookie. But there's no sign of that method in Capybara::RackTest::Browser. But there is an important bit that's easy to miss right up top:

class Capybara::RackTest::Browser
  include ::Rack::Test::Methods

Let's take a peek in Rack::Test::Methods. set_cookie is delegated to the current_session.

METHODS = [
  :request,
  :get,
  :post,
  :put,
  :delete,
  :options,
  :head,
  :follow_redirect!,
  :header,
  :set_cookie,
  :clear_cookies,
  :authorize,
  :basic_authorize,
  :digest_authorize,
  :last_response,
  :last_request
]

def_delegators :current_session, *METHODS

current_session points to rack_test_session:


def current_session # :nodoc:
  rack_test_session(_current_session_names.last)
end

which points to build_rack_test_session:


def rack_test_session(name = :default) # :nodoc:
  return build_rack_test_session(name) unless name

  @_rack_test_sessions ||= {}
  @_rack_test_sessions[name] ||= build_rack_test_session(name)
end

which creates a Rack::Test::Session with a rack_mock_session:


def build_rack_test_session(name) # :nodoc:
  Rack::Test::Session.new(rack_mock_session(name))
end

which points to build_rack_mock_session:


def rack_mock_session(name = :default) # :nodoc:
  return build_rack_mock_session unless name

  @_rack_mock_sessions ||= {}
  @_rack_mock_sessions[name] ||= build_rack_mock_session
end

"Which will bring us back to" Rack::MockSession.


def build_rack_mock_session # :nodoc:
  Rack::MockSession.new(app)
end

And inside Rack::MockSession is where the test calls set_cookie.

def set_cookie(cookie, uri = nil)
  cookie_jar.merge(cookie, uri)
end

And what, finally, is cookie_jar?

def cookie_jar
  @cookie_jar ||= Rack::Test::CookieJar.new([], @default_host)
end

It's probably a newly minted Rack::Test::CookieJar. Now wouldn't it have been a lot easier if I'd just trusted the search results in the first place? find and grep are your best friends. You can trust them to save you a lot of time.