Faking the funk: "Stub" authentication in a Rails Rspec Story
Last week a ran into a bit of an issue trying to write Rspec Stories for the app I am working on. The app in question depends on a remote service based authentication system. Creating “test” users on a remote user authentication service already running as production for other apps was not really an option. Getting around this proved to be problematic as Rspec Stories entire point is to test the “full stack”.
Seeing I had no alternative and really just needed to sidestep authentication here, my first instinct was to try to directly set a user id in a session object:
Story "a fantastic story", "blah", :type => RailsStory do
Scenario "a user does stuff" do
Given "logged in as a user 'someone'" do
@user = User.find_by_login('someone')
session[:user] = @user.id
end
Then "all my tests pass and we live happily ever after" do
get "/users/#{@user.id}"
response.should be_success # login is required here... FAIL!
end
end
end
This does not work as I had guessed. It turns out that the session object you have access to within an Rspec Story is NOT the same object as your actual controllers will look to when you run your story. I am sure there is some hackity way of prying open the Rails stack and modifying the REAL session object, but going down that road just didn’t feel right. My next instinct was to pull in Rspec’s wonderful stubbing/mocking framework and just stub out all the authentication. Bringing mocking into Stories was as easy putting this in my story/helper.rb file
require 'spec/mocks'
We are using a heavily modified version of the restful_authentication plugin, so I just needed to figure out a way to stub #login_required to always return true and #current_user to always return whichever user we want to be currently “logged in”. The problem is that in a Story context we are dealing with potentially ALL controllers… and there really doesn’t seem to be a way to access instances of individual controllers until AFTER a request has been made. My next thought was to stub such that all future instances of ApplicationController would contain the stubbed version of the authentication methods. Im sure this is possible to do, but it also seemed incredibly messy.
This got me thinking: “why am i searching for these complex solutions… let’s just do this the most simple way possible”…
In the context of the restful_authentication plugin all that needs to happen for a user to be “logged_in” is for the user’s ID to be set in a valid session. My solution for getting it there? Monkey patch in a fake login method that can be called within the story:
class AccountsController < ApplicationController
def fake_login
set_session_for_story(params[:login])
redirect_to "/users/#{current_user.login}"
end
end
class ApplicationController
def set_session_for_story(login)
self.current_user = User.find_by_login(login)
end
end
Story "a fantastic story", "blah", :type => RailsStory do
Scenario "a user does stuff" do
Given "logged in as a user 'someone'" do
create_a_test_user(:login => 'someone')
post "/sessions/fake_login", :login => 'someone'
response.should be_redirect
end
Then "all my tests pass and we live happily ever after" do
get "/users/#{@user.id}"
response.should be_success # login is required here... SUCCESS!
end
end
end
This is the simplest way I could come up with for causing the Rails app itself to set the proper user into the session rather than trying to peel back the layers of the app running within the story and manipulate the correct session object directly or trying to deal with stubbing/mocking instances of objects that hadn’t yet been created.
I then took this little bit of hackity and moved it off into another file (/stories/fake_login.rb) so I could then just require it in any story where I needed an authenticated user.