views:

629

answers:

2

I have the following ActiveRecord classes:

class User < ActiveRecord::Base
  cattr_accessor :current_user
  has_many :batch_records
end

class BatchRecord < ActiveRecord::Base
  belongs_to :user

  named_scope :current_user, lambda {
    { :conditions => { :user_id => User.current_user && User.current_user.id } }
  }
end

and I'm trying to test the named_scope :current_user using Shoulda but the following does not work.

class BatchRecordTest < ActiveSupport::TestCase
  setup do
    User.current_user = Factory(:user)
  end

  should_have_named_scope :current_user,
                          :conditions => { :assigned_to_id => User.current_user }
end

The reason it doesn't work is because the call to User.current_user in the should_have_named_scope method is being evaluated when the class is being defined and I'm change the value of current_user afterwards in the setup block when running the test.

Here is what I did come up with to test this named_scope:

class BatchRecordTest < ActiveSupport::TestCase
  context "with User.current_user set" do
    setup do
      mock_user = flexmock('user', :id => 1)
      flexmock(User).should_receive(:current_user).and_return(mock_user)
    end

    should_have_named_scope :current_user,
                            :conditions => { :assigned_to_id => 1 }
  end
end

So how would you test this using Shoulda?

+1  A: 

I think you are going about this the wrong way. Firstly, why do you need to use a named scope? Wont this just do?

class BatchRecord < ActiveRecord::Base
  belongs_to :user

  def current_user
    self.user.class.current_user
  end
end

In which case it would be trivial to test. BUT! WTF are you defining current_user as a class attribute? Now that Rails 2.2 is "threadsafe" what would happen if you were running your app in two seperate threads? One user would login, setting the current_user for ALL User instances. Now another user with admin privileges logs in and current_user is switched to their instance. When the first user goes to the next page he/she will have access to the other persons account with their admin privileges! Shock! Horror!

What I reccomend doing in this case is to either making a new controller method current_user which returns the current user's User instance. You can also go one step further and create a wrapper model like:

class CurrentUser

  attr_reader :user, :session

  def initialize(user, session)
    @user, @session = user, session
  end

  def authenticated?
    ...
  end

  def method_missing(*args)
    user.send(*args) if authenticated?
  end

end

Oh, and by the way, now I look at your question again perhaps one of the reasons it isn't working is that the line User.current_user && User.current_user.id will return a boolean, rather than the Integer you want it to. EDIT I'm an idiot.

Named scope is really the absolutely wrong way of doing this. Named scope is meant to return collections, rather than individual records (which is another reason this fails). It is also making an unnecessary call the the DB resulting in a query that you don't need.

Chris Lloyd
Chuck
Whoops, you're right! Early morning drinking wasn't helping with my Ruby skills.
Chris Lloyd
A: 

I just realized the answer is staring right at me. I should be working from the other side of the association which would be current_user.batch_records. Then I simply test the named_scope on the User model and everything is fine.

@Chris Lloyd - Regarding the thread safety issue, the current_user attribute is being set by a before_filter in my ApplicationController, so it is modified per request. I understand that there is still the potential for disaster if I chose to run in a multi-threaded environment (which is currently not the case). That solution I suppose would be another topic entirely.

Matt Haley
http://m.onkey.org/2008/10/23/thread-safety-for-your-rails#comment-2601
Chris Lloyd
I was previously having to constantly pass around current_user, I suppose after reading that, I'll just have to live with it. Thanks for your comments.
Matt Haley