I have an ActiveModel::Serializer class (class TaskSerializer < ActiveModel::Serializer) that I'm looking to test.
The serializer makes use of the current_user object, because it's in scope during the controller actions.
But I'm attempting to write a new rspec file. (task_serializer_spec.rb)
When I run
TaskSerializer.new(task).to_json
I get an error saying that the current_user method doesn't exist.
I can't mock the variable because we have "the method must exist" flag on our mocks.
I understand that there are some other parameters that I can pass in the NEW. but I can't find any docs on it. Can someone offer a way to get things like current_user in the scope.
My answer might be a little late, but if someone ever ends up on this page, here is a way to do this:
app/serializers/task_serializer.rb :
class TaskSerializer < ActiveModel::Serializer
attributes :id, :method_using_current_user, :whatever_other_attributes_goes_here
delegate :current_user, to: :scope
def method_using_current_user
current_user.some_method_here
end
end
In your rspec test, you can than do something like this:
RSpec.describe TaskSerializer do
let(:user) { create(:user) }
# or any other user you want to create here
before do
# As current user is delegated to controller scope, we mock both here
allow_any_instance_of(TaskSerializer).to receive(:scope).and_return(ApplicationController.new)
allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user)
end
# your test goes here
end
And voila.
Related
I've got a method defined in ApplicationController as a helper method.
helper_method :can_access_participant_contact_data?
I'm trying to write a test for a helper method that resides in a helper file. This helper method makes a call to helper_method :can_access_participant_contact_data?
# In participants_helper.rb
#
def redacted_contact_data participant, attribute_name
attribute = participant.try(:contact_data).try(attribute_name)
return attribute if can_access_participant_contact_data?(participant)
return nil if attribute.blank?
return attribute.gsub(/\S/i, '*') # Asterisked string
end
All I'm doing so far in my test is making a call to redacted_contact_data
require 'test_helper'
class ParticipantsHelperTest < ActionView::TestCase
test "should return an asterisked string with spaces" do
redacted_contact_data(Participant.first, :name)
end
end
When I run my test, I'm getting this message
undefined method `can_access_participant_contact_data?' for #<ParticipantsHelperTest:0x007fd6c7c6d608>
I've been having a look around but I'm not sure how to get around this issue. Do I need to mock can_access_participant_contact_data? somehow? or can I just include the method into the test?
AFAIK (As far as I know), you cannot fix this without stubbing, or doing some change in your code, as essentially a helper file is just a module of itself that should be treated independent of where it's gonna be included. Who knows you might want to include such helper file inside your model files for example, in which incidentally the model file also has a method named can_access_participant_contact_data? but does differently from that one defined in the ApplicationController, therefore you cannot unit test this without specifying the context / base.
Possible Workarounds:
Stubbing:
Use Mocha or rework testing into RSpec
Or manually (maybe there's a better way) by:
test "should return an asterisked string with spaces" do
ParticipantsHelper.class_eval do
define_method :can_access_participant_contact_data? do |arg|
true
end
end
redacted_contact_data(Participant.first, :name)
end
Or, moving all your ApplicationController helper methods into a separate/existing helper file, say inside your already existing ApplicationHelper. Then afterwards, include that helper inside your other helper file that you are testing that is making use of the method/s. i.e.:
# helpers/application_helper.rb
module ApplicationHelper
def can_access_participant_contact_data?(participant)
# YOUR CODE
end
end
# helpers/participants_helper.rb
module ParticipantHelper
include ApplicationHelper
def redacted_contact_data participant, attribute_name
attribute = participant.try(:contact_data).try(attribute_name)
return attribute if can_access_participant_contact_data?(participant)
return nil if attribute.blank?
return attribute.gsub(/\S/i, '*') # Asterisked string
end
end
If using this approach, then two ways to call the helper method inside the controller:
Use Rails helpers method inside a controller:
class ParticipantsController
def show
helpers.can_access_participant_contact_data?(#participant)
end
end
Or, include the helpers directly (I personally prefer the other approach just above)
class ApplicationController < ActionController::Base
include ApplicationHelper
end
class ParticipantsController < ApplicationController
def show
can_access_participant_contact_data?(#participant)
end
end
For the view files, you won't need to update any code.
Another idea is to do "helper test" in "controller test" as follows:
require 'test_helper'
class ParticipantsControllerTest < ActionController::TestCase
setup do
# do some initialization here. e.g. login, etc.
end
test "should return an asterisked string with spaces" do
participant = ...
get :show, id: participant.id
assert_equal '...', #controller.view_context.redacted_contact_data(...)
end
end
Where, #controller is ParticipantsController object already defined by rails controller testing framework (or you can explicitly define it when controller name is different from *ControllerTest), and view_context is the object for helper methods (see https://api.rubyonrails.org/classes/ActionView/Rendering.html#method-i-view_context for more detail).
Helper method often refer controller object and/or method (like session, request) so that it is sometimes difficult to do unit-test only in test/helpers/*. This is the reason why I test helper in controller in such a case.
Rails form validation is designed to go in the model most easily. But I need to make sure the current user has the required privileges to submit a post and the current_user variable is only accessible in the controller and view.
I found this answer in a similar question:
You could define a :user_gold virtual attribute for Book, set it in the controller where you have access to current_user and then incorporate that into your Book validation.`
How can I set this up with my post and user controller so that the current_user variable is accessible in the model?
Solution:
This whole thing is wrong from an application design perspective as #Deefour's answer pointed out. I changed it so my view doesn't render the form unless the condition is true.
The "similar question" is saying you can do something like this
class YourModel < ActiveRecord::Base
attr_accessor :current_user
# ...
end
and then in your controller action you can do something like
#your_model = YourModel.find(params[:id])
#your_model.current_user = current_user
#your_model.assign_attributes(params[:your_model])
if #your_model.valid?
# ...
You can then use self.current_user within YourModel's validation methods.
Note I don't think this is what you should be doing though, as I don't consider this "validation" as much as "authorization". An unauthorized user shouldn't even be able to get the part of your action where such an update to a YourModel instance could be saved.
As for doing the authorization with Pundit as requested, you'd have a file in app/policies/your_model.rb
class YourModelPolicy < Struct.new(:user, :your_model)
def update?
user.some_privilege == true # change this to suit your needs, checking the "required privileges" you mention
end
end
Include Pundit in your ApplicationController
class ApplicationController < ActionController::Base
include Pundit
# ...
end
Then, in your controller action you can do simply
def update
#your_model = YourModel.find(params[:id])
authorize #your_model
# ...
The authorize method will call YourModelPolicy's update? method (it calls the method matching your action + ? by default) and if a falsy value is returned a 403 error will result.
Authorization shouldn't be done in models. Models have already many responsibilities don't you think?
That's a controller thing, and actually you can have the logic in other place using some gem like cancan and in your controller you would do something like:
authorize! :create, Post
You can define a "virtual attribute" in your model like this:
class Book < ActiveRecord::Base
attr_accessor :current_user
end
Its value can be set directly in your controller like this:
class BooksController < ApplicationController
def create
book = Book.new
book.current_user = current_user
book.save!
end
end
And inside your model's validation routine, you can access it like any other ActiveRecord field:
def validate_user_permission
errors[:current_user] = "user does not have permission" unless current_user.is_gold?
end
I can't remember if this is the case with ActiveRecord, but you might be able to set virtual attributes via the mass-assignment methods like create, update, and new in the controller:
def create
Book.create!(current_user: current_user)
end
In order to do that, you would probably have to add the following line to your model to enable mass-assignment of that virtual attribute:
attr_accessible :current_user
I agree with Ismael - this is normally done in the controller. It's not an attribute of the model, it's a permission issue and related to the controller business logic.
If you don't need all the power of a gem like CanCan, you can role your own.
class BooksController < ApplicationController
before_filter :gold_required, :only => :create
def create
book = Book.new
book.save!
end
# Can be application controller
private
def gold_required
return current_user && current_user.is_gold?
end
end
You may want to put the filter on the 'new' method as well.
I've got a model class that overrides update_attributes:
class Foo < ActiveRecord::Base
def update_attributes(attributes)
if super(attributes)
#do some other cool stuff
end
end
end
I'm trying to figure out how to set an expectation and/or stub on the super version of update_attributes to make sure that in the success case the other stuff is done. Also I want to make sure that the super method is actually being called at all.
Here's what I have tried so far (and it didn't work, of course):
describe "#update_attributes override" do
it "calls the base class version" do
parameters = Factory.attributes_for(:foo)
foo = Factory(:foo, :title => "old title")
ActiveRecord::Base.should_receive(:update_attributes).once
foo.update_attributes(parameters)
end
end
This doesn't work, of course:
Failure/Error: ActiveRecord::Base.should_recieve(:update_attributes).once
NoMethodError:
undefined method `should_recieve' for ActiveRecord::Base:Class
Any ideas?
update_attributes is an instance method, not a class method, so you cannot stub it directly on ActiveRecord::Base with rspec-mocks, as far as I know. And I don't think that you should: the use of super is an implementation detail that you shouldn't be coupling your test to. Instead, its better to write examples that specify the behavior you want to achieve. What behavior do you get from using super that you wouldn't get if super wasn't used?
As an example, if this was the code:
class Foo < ActiveRecord::Base
def update_attributes(attributes)
if super(attributes)
MyMailer.deliver_notification_email
end
end
end
...then I think the interesting pertinent behavior is that the email is only delivered if there are no validation errors (since that will cause super to return true rather than false). So, I might spec this behavior like so:
describe Foo do
describe "#update_attributes" do
it 'sends an email when it passes validations' do
record = Foo.new
record.stub(:valid? => true)
MyMailer.should_receive(:deliver_notification_email)
record.update_attributes(:some => 'attribute')
end
it 'does not sent an email when it fails validations' do
record = Foo.new
record.stub(:valid? => false)
MyMailer.should_receive(:deliver_notification_email)
record.update_attributes(:some => 'attribute')
end
end
end
Try replacing should_recieve with should_receive.
I have this code in a model called Project. It sets a owner to a project before the project is saved.
before_save :set_owner
# Set the owner of the project right before it is saved.
def set_owner
self.owner_id = mock_model(User).id # current_user.id is stubbed out for a mock_model.
# Lifecycle is set by the form's collection_select
end
And current_user function is stubbed out in my Rspec tests to return a mock_model (which is why the code above is showing mock_model instead of current_user).
Now, when I run this, my Rspec tests break, and complains:
undefined method `mock_model' for #<Project:0x105c70af0>
My guess, is that since before_save is a instance function, it somehow thinks that mock_model is a function defined in Project.
Someone must have encountered this before... Any way around it?
Two things immediately stand out:
You shouldn't be using mock_model in your actual Project model. All test code should remain in the specs.
You cannot pass the current_user object from the controller to the model (at least not in any way you should).
I would use an attr_accessor in your project model to set the current_user id.
class Project < AR::Base
attr_accessor :current_user
def set_owner
self.owner_id = current_user.id unless current_user.nil?
end
end
Then your spec should look something more along the lines of:
it "should set the owner id" do
user = mock_model(User)
project = Project.new
project.current_user = user
project.save
project.owner_id.should == user.id
end
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?
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.
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.