Interesting. The book I'm going through for rails has added login functionality to adding a new story to the site. I've added a before_filter for the new action on StoryController.
before_filter :login_required, :only => :new
Now, though, my StoryController tests fail that look at getting the new form. The :login_required filter looks like
def logged_in?
! @current_user.blank?
end
helper_method :logged_in?
def login_required
return true if logged_in?
session[:return_to] = request.request_uri
redirect_to :controller => "/account", :action => "login" and
return false
end
Well, unfortunately, in order to get a user logged in, I need to call the :login action on the AccountController. The book didn't include this, so my functional tests for StoryController are failing, as a test like
def test_should_show_new_form
get :new
assert_select 'form p', :count => 3
end
doesn't have form with 3 p elements. I had a thought that it was getting redirected, so I changed the test to
def test_should_show_new_form
get :new
assert_redirected_to :controller=>"/account", :action=>'login'
end
and it passes. So, it appears that the before_filter is working.
So, I need to fake out the filter somehow; I need to set up a logged on user. The log on looks like:
before_filter :fetch_logged_in_user
protected
def fetch_logged_in_user
return if session[:user_id].blank?
@current_user = User.find_by_id(session[:user_id])
end
I am not 100% sure if I need to fake the session[:user_id] or just have a @current_user. I tried both, actually, and
session[:user_id] = 1
throws an exception in the test.
@current_user = users(:corey)
doesn't work either.
I figured that I could call the login action with credentials from users(:corey). I first tried
post :controller=>'/account',:action=>'login', :login => users(:corey).login, :password => users(:corey).password
at the top of the my test, hoping that it would call the action and get my system logged in. Here's the exception I got:
ActionController::UnknownAction: No action responded to passwordCorey Hainesactionlogincontroller/accountlogincorey
That is a really strange error. Looking at it, I think my understanding of the users fixture may not be getting what I think, so I'll try this:
post :controller=>'/account',:action=>'login', :login => 'patrick', :password => 'sekrit'
Nope, that didn't work either. So, my I do understand users. Looking closer at the exception, I'm guessing that I may just have the parameters for post wrong.
Off I go to http://api.rubyonrails.com/ to look up post.
post(path, parameters=nil,headers=nil)
Doesn't look like the post that I'm using. Doh! Of course it isn't, since I'm using the post from the testing framework. Looking up on google, I finally find a post called "#post method in tests with a different controller."
It turns out that you need to redefine the controller, then call post, then reset the controller. Let's try it. I changed the test to the following code:
def test_should_show_new_form
old_controller = @controller
@controller = AccountController.new
post :login, :login => 'patrick', :password => 'sekrit'
@controller = old_controller
get :new
assert_select 'form p', :count => 3
end
I ran the test, and, voila, it worked! So, now, I just need to add this to the other tests that are failing. I have a test to check adding a story:
def test_should_add_story
post :new, :story => {
:name=>'test story',
:link=>'http://www.test.com/'
}
assert ! assigns(:story).new_record?
assert_redirected_to :action=>'index'
assert_not_nil flash[:notice]
end
Let me change it to support logging in:
def test_should_add_story
old_controller = @controller
@controller = AccountController.new
post :login, :login => 'patrick', :password => 'sekrit'
post :new, :story => {
:name=>'test story',
:link=>'http://www.test.com/'
}
assert ! assigns(:story).new_record?
assert_redirected_to :action=>'index'
assert_not_nil flash[:notice]
end
HUZZAH! It worked! Let me pull it out into a new method:
def login_user
old_controller = @controller
@controller = AccountController.new
post :login, :login => 'patrick', :password => 'sekrit'
@controller = old_controller
end
This seems like a fantastic helper method execute_under_different_controller(controller, code). I'm not 100% sure how to build this method, so I'm going to take a minute to learn how to do it in Ruby.
We now have a nice method called execute_different_controller, so changed login_user to
def login_user
execute_under_different_controller AccountController do
post :login, :login => 'patrick', :password => 'sekrit'
end
end
I don't like the fact that I'm duplicating the data from my users.yaml file in my tests. If I load the fixture, I should be able to reference users(:patrick) here. Let's try it.
def login_user
user = users(:patrick)
execute_under_different_controller AccountController do
post :login, :login => user.login, :password => user.password
end
end
Well, that worked. I guess I can understand that the book doesn't push too hard on this point, but it is a crazy, dangerous amount of duplication in your tests. This sort of data duplication leads to fragile tests.
After adding login_user to the tests for StoryController, things are back on track to continue through the book.