Staying DRY while testing a controller, authorized via CanCan
I'm retroactively writing some tests, using RSpec, for a Rails project.
I'm using the CanCan gem to provide authorization. I decided to write a spec that will test the ability.rb
model.
I then went on to test my remaining models.
I've moved on to controllers, and I've run into a huge snag: I'm testing my abilities all over again!
Basically, I have to stub out a series of models, and stub out their associations; otherwise the response just returns 403 Forbidden
.
I'm not quite sure where to go from here. I'm stubbing out up to 6 models, just to write a single test.
I know the abilities work, that's what ability_spec.rb
is for.
So this question is really 2-fold:
- Should I be testing the ability model separately?
- Should the controller tests be concerned with proper permissions?
Edit require 'spec_helper' include Devise::TestHelpers # to give your spec access to helpers
describe TokensController do
before(:each) do
@mock_user = User.new(:username => "bob", :email => "user@user.com", :password => "longpas开发者_如何学运维sword")
@mock_user.role = "admin"
sign_in @mock_user
#Ability.stub!('can').and_return(true)
end
it "should let me see grids/:g_id/tokens index" do
test_grid = mock_model(Grid)
test_token = mock_model(Token)
Grid.stub!(:find).and_return(test_grid)
Token.stub!(:find).and_return(test_token)
get 'index'
a1 = Ability.new(@mock_user)
a1.can?(:index, Token).should be_true # This line works fine; as it should
puts response.status #This returns 403, which means CanCan::AccessDenied was raised
end
end
Thanks,
RobbieNot sure if this is too late for you, but I just ran into the same issue, and solved it using the following code sample --
before do
@user = Factory.create(:user)
sign_in @user
@abilities = Ability.new(@user)
Ability.stub(:new).and_return(@abilities)
end
end
I've stubbed out Ability#new, giving me a reference to the instance of Ability that controls the current user. Then, I can stub out specific abilities like this:
@abilities.stub!(:can?).with(:destroy, regatta).and_return(true)
or give admin privileges:
@abilities.stub!(:can?).and_return(false)
I do test the cancan model separately, but testing what it will allow in what conditions.
I think if you are doing things like
authorize! :take_over, @the_world
Then I do think you should be testing that in the controller. I'm not sure you need to test ALL 6 versions of your models though.
You can stub out the Ability.can? class and have it respond true/false, and test how your controller handles when it can (and more importantly) when it cannot continue.
Similar to Sam's answer, but from the CanCan wiki page on testing:
Controller Testing
If you want to test authorization functionality at the controller level one option is to log-in the user who has the appropriate permissions.
user = User.create!(:admin => true) # I recommend a factory for this
# log in user however you like, alternatively stub `current_user` method
session[:user_id] = user.id
get :index
assert_template :index # render the template since he should have access
Alternatively, if you want to test the controller behavior independently from what is inside the Ability class, it is easy to stub out the ability with any behavior you want.
def setup
@ability = Object.new
@ability.extend(CanCan::Ability)
@controller.stubs(:current_ability).returns(@ability)
end
test "render index if have read ability on project" do
@ability.can :read, Project
get :index
assert_template :index
end
If you have very complex permissions it can lead to many branching possibilities. If these are all tested in the controller layer then it can lead to slow and bloated tests. Instead I recommend keeping controller authorization tests light and testing the authorization functionality more thoroughly in the Ability model through unit tests as shown at the top.
I think authorization needs to be done mainly for controllers to make sure your authorization is working correctly with your controllers. So to make it DRY you can implement your own matcher
to be used like this
let!(:user) {create :user}
before { login_user_request user}
it "grants admin access to show action" do
expect{ get :show, {id: user.id} }.to be_authorized
end
it "denies user access to edit action" do
expect{ get :edit, {id: user.id} }.to be_un_authorized
end
and then implement these matchers with your own way to test how a request will be authorized or not
RSpec::Matchers.define :be_authorized do
match do |block|
block.call
expect(response).to be_success
end
def supports_block_expectations?
true
end
end
RSpec::Matchers.define :be_un_authorized do
match do |block|
expect{
block.call
}.to raise_error(Pundit::NotAuthorizedError)
end
def supports_block_expectations?
true
end
end
Why don't you include a
can :manage, :all do
user.is_ultrasuper == 1
end
in your Ability and then have a is_ultrasuper param in one of your fixture users:
one:
id: 1
username: my_username
is_ultrasuper: 1
Then log this user in at setup of your tests. This in tests you should be able to do anything at all.
精彩评论