I'm using CanCanCan with Rolify and I´m trying to test my Ability class authorization.
When testing if a unprivileged user can CRUD other users in the system the test fails
1) Ability a guest user should not be able to manage others
Failure/Error: expect(subject).to_not be_able_to(:crud, User)
expected not to be able to :crud User(...)
But I can't find any reason why the check in my Ability class fails:
class Ability
include CanCan::Ability
def initialize(user = User.new)
alias_action :create, :read, :update, :destroy, :destroy_multiple, to: :crud
# What is wrong?
can :crud, User, id: user.id
if user.has_role?(:admin)
can :manage, User
end
end
end
This is my spec:
require 'rails_helper'
require 'cancan/matchers'
RSpec.describe Ability do
let(:user) { create(:user) }
subject { Ability.new(user) }
context "a guest user" do
it "should be able to manage self" do
expect(subject).to be_able_to(:crud, user)
end
it "should not be able to manage others" do
expect(subject).to_not be_able_to(:crud, User)
end
end
end
expect(subject).to_not be_able_to(:crud, User)
You are referencing User model, not instance there. Use User.new or another persisted User instance.
Related
I have this in models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
if user.role? :registered
can :read Post
end
end
When I do this on rails console
#this returns a user with a role: "registered" attribute
user = User.first
post = Post.first
ability = Ability.new(user)
### This returns false ###
ability.can?(:read, post)
#=> false
This spec I have written to test the ability also fails while i expect it to pass.
describe User, :type => :model do
let(:post) {create(:post)}
describe "abilities" do
subject(:ability){Ability.new(user)}
let(:user){nil}
context "when is a registered user" do
## the default value for the role attribute in the user factory is "registered"
let(:user) {create(:user)}
it {is_expected.to be_able_to :read, post}
end
end
end
I can access and read posts in both /posts and /posts/:id when I am authenticated as a registered user on the browser, I have no idea why it is failing in both rails console and rspec.
Following our discussion, we concluded that the problem is either
Rails didn't load the Ability class, or
A code somewhere somehow overrides the Ability class.
The workaround-solution is to manually load the Ability file by appending the following at the end of the application.rb
require "#{Rails.root}/app/models/ability.rb"
I am using cancan gem.
I have ability defined in my class:
module Gexcore
class Myability
include CanCan::Ability
def initialize(user)
can :delete_user, User do |victim|
..
end
end
end
end
I am testing the ability to delete user with RSpec:
RSpec.describe "Permissions to delete user", :type => :request do
describe 'del user' do
it 'superadmin can delete user' do
superadmin = double(User, :id => 10, <<some other fields>>)
ability = Mymodule::Myability.new(superadmin)
victim = double(User)
res = superadmin_ability.can?(:user_del, victim)
expect(res).to eq true
end
end
end
My user model:
class User < ActiveRecord::Base
delegate :can?, :cannot?, :to => :ability
...
end
But it doesn't see method can? :delete_user, User from my ability.
It expects User class but RSpec calls it with RSpec::Mocks::Double class.
How to test correctly ability from cancan gem using Rspec mocks ?
RSpec is using a Double class because that's where it ends in your code. The class must be User for RSpec to apply the rules.
let(:user) do
u = User.new
u.stub!(:some_qualifier_in_your_delete_user_declaration? => true)
u
end
See this SO archive for more of a reference.
Here's my very simple ability class:
class Ability
include CanCan::Ability
def initialize(user)
if user.has_role? :admin
can :manage, :control_panel
end
end
end
How should I mock it in a controller spec?
Here's my control panel controller:
class Admin::ControlPanelController < ApplicationController
authorize_resource class: false
rescue_from CanCan::AccessDenied do |exception|
redirect_to root_url, danger: "#{exception}"
end
def statistics
end
end
Here's my control_panel controller spec:
describe '#statistics:' do
let(:request){ get :statistics }
context 'When guest;' do
before do
# HOW SHOULD I MOCK HERE?
end
describe 'response' do
subject { response }
its(:status){ should eq 302 }
its(:content_type){ should eq 'text/html' }
it{ should redirect_to root_path }
end
describe 'flash' do
specify { expect( flash[:danger] ).to eq "You do not have sufficient priviledges to access the admin area. Try logging in with an account that has admin priviledges." }
end
end
How should I mock the ability? Before, I was doing this:
let(:user){ FactoryGirl.create :user }
expect(controller).to receive(:current_user).and_return user
expect(user).to receive(:has_role?).with(:admin).and_return false
but that was before I was using cancan and was manually checking that the user had a certain role. This behaviour was happening in the application controller and so was very easy to mock. I'm having difficulty mocking this ability class :(
I want to mock it in different contexts. I'm feeling a bit lost because even if I do this:
expect(Ability).to receive(:asdasdadskjadawd?).at_least(:once)
No error is raised, though one is raised if I do spell 'Ability' wrongly so it's mocking the class ok...
I don't think you should be mocking the Ability class, especially not in a controller test. The Ability class is more like configuration than code; it doesn't change during your application. It's also an implementation detail that the controller shouldn't care about.
Instead, you should be mocking your Users. It looks like you're using FactoryGirl; you could use FactoryGirl's traits to mock the various kinds of user you have:
FactoryGirl.define do
factory :user do
name 'Bob'
email 'bob#example.com
role 'user'
trait :admin do
role 'admin'
end
trait :guest do
role 'guest'
end
end
end
You can then use FactoryGirl.create :user if you need a regular user, and FactoryGirl.create :user, :admin if your test requires an admin.
I am currently using cancan with rspec.
Please take a look at my ability.rb
require 'spec_helper'
require "cancan/matchers"
class Ability
include CanCan::Ability
def initialize(user)
if user # Only logged in users
if user.role? :admin
can :manage, :all
elsif user.role? :producer
can :read, Business
can :update, Business do |b|
b.user_id == user.id
end
can :redeem, Purchase
elsif user.role? :consumer
can :your, Deal
can [:create, :redirect_to_wepay], Purchase
can :show, Purchase do |purchase|
purchase.user_id == user.id
end
end
# Good thing about devise with Cancan is that it takes care of this.
can :manage, User do |the_user|
the_user.id == user.id
end
else
# This is needed for the cans that follows
user = User.new
end
# Everyone's session
can :read, Deal
can :read, Business
# You have to enable it for wepay
can [:sold_out, :callback, :received], Purchase
end
end
In my spec/models/ability_spec.rb I have
describe Ability do
describe "consumers" do
describe "cancan" do
before(:each) do
#user = Factory(:user, :role => "consumer")
#ability = Ability.new(#user)
end
describe "success" do
#**This line I am getting ability is nil
#ability.should == 5
#**This line gives me be_able_to undefined
##ability.should_not be_able_to(:read, Factory(:deal))
##ability.can(:read, Factory(:business)).should be_true
end
Any ideas why I am getting #ability as nil?
In addition, I want to put some of my controller's actions that are related to permission control in this ability_spec.rb file. Is that possible? (I explicitly want to achieve this because my app has 3 roles of users and I find myself littering my controllers spec files with all these permission related one liners.
Thanks!
Tests must appear in it or specify blocks. describe and context are simply for grouping.
describe "success" do
#**This line I am getting ability is nil
#ability.should == 5
end
Should be more like:
it "allows consumers to do blah blah blah" do
#ability.should == 5
end
I'm (finally) wiring CanCan / Ability into my app, and I've started by writing the RSpec tests. But they're failing — my Abilities appear to be overly permissive, and I don't understand why.
First, the Ability class. The intention is that non-admin users can manage only themselves. In particular, they cannot look at other users:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # create guest user if needed
if (user.has_role?(:admin))
can(:manage, :all)
else
can(:manage, User, :id => user.id)
end
end
end
The RSpec tests:
require 'spec_helper'
require 'cancan/matchers'
describe Ability do
before(:each) do
#user = User.create
end
describe 'guest user' do
before(:each) do
#guest = nil
#ability = Ability.new(#guest)
end
it "should_not list other users" do
#ability.should_not be_able_to(:read, User)
end
it "should_not show other user" do
#ability.should_not be_able_to(:read, #user)
end
it "should_not create other user" do
#ability.should_not be_able_to(:create, User)
end
it "should_not update other user" do
#ability.should_not be_able_to(:update, #user)
end
it "should_not destroy other user" do
#ability.should_not be_able_to(:destroy, #user)
end
end
end
All five of these tests fail. I've read the part of Ryan's documentation where he says:
Important: If a block or hash of
conditions exist they will be ignored
when checking on a class, and it will
return true.
... but at most, that would only explain two of the five failures. So clearly I'm missing something fundamental.
I would expect this to work:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # create guest user if needed
if (user.has_role?(:admin))
can(:manage, :all)
elsif user.persisted?
can(:manage, User, :id => user.id)
end
end
end
I'm not sure what the behavior is defined to be if you pass :id => nil, which is what happens in the guest case, but at any rate, if you don't want the guest to access the list view, you shouldn't call can :manage, User for that user at all.
In general, I find that assigning user ||= User.new to make the ability harder to reason about.
Hey, apparently this should work, but some refactoring would help you to find the issue:
require 'spec_helper'
require 'cancan/matchers'
describe Ability do
before(:each) { #user = User.create }
describe 'guest user' do
before(:each) { #ability = Ability.new(nil) }
subject { #ability } # take advantage of subject
it "should not be an admin user" do
#user.should_not be_admin
#user.should be_guest
end
it "should_not show other user" do
should_not be_able_to(:read, #user)
end
it "should_not create other user" do
should_not be_able_to(:create, User)
end
it "should_not update other user" do
should_not be_able_to(:update, #user)
end
it "should_not destroy other user" do
should_not be_able_to(:destroy, #user)
end
end
end
Note that also I removed this example #ability.should_not be_able_to(:read, User).
Hope it helps you.
I've got this bad habit of answering my own questions, but I give props to #jpemberthy and #Austin Taylor for pointing me in the right direction. First (and this is cosmetic), I added this to my User model:
class User
...
def self.create_guest
self.new
end
def guest?
uninitialized?
end
end
and cleaned up my Abilities model accordingly:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.create_guest
if (user.admin?)
<admin abilities here>
elsif (user.guest?)
<guest abilities here>
else
<regular user abilities here>
end
end
end
But the real fix was in my RSpec tests. Since User has validations on email and password fields, my original code of:
before(:each) do
#user = User.create
end
was failing, thus creating an uninitialized #user. Since the :id field was nil, the Ability clause:
can(:manage, User, :id => user.id)
was succeeding with a guest user because nil == nil (if that makes sense). Adding the required fields to satisfy the User validations made (almost) everything work.
Moral: just as #jpemberthy suggested in his code, always include a test to make sure your user objects have the privileges that they are supposed to! (I still have another question regarding CanCan, hopefully less boneheaded than this one, appearing in a StackOverflow topic near you...)