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.
Merb CRUD - ie: how to properly destroy things
By now most Rails developers have come to know and love (or not?) RESTful resources and the controller code that implements them. For the most part Merb’s implementation of resources will be familiar and adheres to the same convention - GET safe creates, updates, destroys, a single base URI for a single resource, etc.
One small difference with Merb’s resources is in regards to the implementation of destroying a resource. In Rails you run into this weird situation where performing a destroy on a record via a browser is nearly impossible without relying on Javascript to create that ugly dynamic form that gets triggered by using the link_to method and passing “:method => :delete”.
<%= link_to "Delete Image", { :action => "delete", :id => @image.id }, :confirm => "Are you sure?", :method => :delete %>
# => <a href="/testing/delete/9/" onclick="if (confirm('Are you sure?')) { var f = document.createElement('form');
f.style.display = 'none'; this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href;
var m = document.createElement('input'); m.setAttribute('type', 'hidden'); m.setAttribute('name', '_method');
m.setAttribute('value', 'delete'); f.appendChild(m);f.submit(); };return false;">Delete Image</a>
Not ideal IMHO.
The other option in Rails is to create the delete link manually as a button or submit tag in its own little form - personally i use this approach because it is does not require Javascript to work, but I have never liked cluttering the DOM with tons of little forms.
Merb, has a different answer - one that tripped me up until I realized what was going on: use an intermediary page.
In Merb a GET request to /users/1/delete (with the users resource declared in the router) will route to the “delete” method in the users controller. The purpose of this page is exactly the same as the “new” and “edit” methods - to provide an HTML form to someone (or something) interacting with the resource via a web browser. The idea is that in a non-Javascript world you can still implement an “are you sure?” type confirmation AND get the added benefit of providing a GET safe destroy. If the developer would like to mimic the Rails inline javascript delete form style html link it is trivial to do so, but with the added benefit of graceful degradation (ie: still works without javascript).
The ideal solution in my humble opinion is first implement the full delete page with destroy html form and “Are you sure?” confirmation message and then after that is working (and tested). Next you would implement an Ajax destroy button that posts a DELETE request and will be routed directly to the destroy method in the controller. This way you get a great user experience when using a fully featured web browser, but are still able to use the app if you happen to not have javascript.
Headed to Portland!
So its finally official… I am headed out to RailsConf in Portland, OR next week!