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… lets just do this the most simple way possible”…
In the context of restful_authentication 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.
Headed to Portland!
So its finally official… I am headed out to RailsConf in Portland, OR next week!
Fun with Ruby blocks, modules, class inheritance, and “super”
The situation: A class, it’s descendant, and a module included in the parent class all have a method of the same name. Calling super from the child’s method causes a call to the parent class and not the module…
module Something
def foo(&block)
puts "Hello Module,"
yield
puts "Goodbye Module... it was nice knowing you. "
end
end
class ParentFun
include Something
def foo(&block)
puts "Hello Parent,"
yield
puts "Goodbye Parent.. it was nice knowing you"
end
end
class ChildFun < ParentFun
def foo
super do
puts "I am a Child."
end
end
end
ChildFun.new.foo #=>
# Hello Parent,
# I am a Child.
# Goodbye Parent.. it was nice knowing you
Now put the module include in the Child class instead of Parent.
module Something
def foo(&block)
puts "Hello Module,"
yield
puts "Goodbye Module... it was nice knowing you. "
end
end
class ParentFun
def foo(&block)
puts "Hello Parent,"
yield
puts "Goodbye Parent.. it was nice knowing you"
end
end
class ChildFun < ParentFun
include Something
def foo
super do
puts "I am a Child."
end
end
end
ChildFun.new.foo #=>
# Hello Module,
# I am a Child.
# Goodbye Module... it was nice knowing you.
Interesting.
Managing branches in SVN
-
Requirements and Dependencies
- svnmerge.py - a python script that helps you keep your branches in synch with the trunk. The script and full documentation are available at http://www.orcaware.com/svn/wiki/Svnmerge.py
- define a bash variable of the svn path ($TIS_SVN_URL in this case)
-
Creating the branch
- stop your development server
- commit any outstanding changes to the trunk
- copy the trunk to a branch
svn copy $TIS_SVN_URL/trunk $TIS_SVN_URL/branches/branch-name
- switch over to that branch
svn switch $TIS_SVN_URL/branches/branch-name
- initialize the branch
svnmerge.py init
- commit:
svn commit -F svnmerge-commit-message.txt
-
Working with the branch
- stop your development server
- occasionally perform svnmerge.py merge to keep in sync with the trunk
-
Merging branch back into the trunk
- stop your development server
- with a clean branch switch over to the trunk:
svn switch $TIS_SVN_URL/trunk
- check properties on trunk for any merge stuff:
svn proplist -v .
- Initialize the merge tracking support on the trunk, related to the given branch, using:
svnmerge.py init $TIS_SVN_URL/branches/alpha02-dashboard
- commit:
svn commit -F svnmerge-commit-message.txt
- update
- merge:
svnmerge.py merge --bidirectional -S $TIS_SVN_URL/branches/alpha02-dashboard > merge_log.txt
- check merge log for any conflicts
- resolve any conflicts
- rake test
- commit:
svn commit -F svnmerge-commit-message.txt
- svnmerge.py uninit -S $TIS_SVN_URL/branches/alpha02-dashboard
- commit:
svn commit -F svnmerge-commit-message.txt
- svn rm -m ‘removing branch’ $TIS_SVN_URL/branches/alpha02-dashboard