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...)
Related
I have two user roles and i want to write a test in rspec to see if the user role are attached to the user after creation. Also admins should be able to switch the user roles from one to the other.
Here is my user model how i structure the type of users using a enum
enum role: [:batman, :superman]
after_initialize :set_default_role, :if => :new_record?
protected
def set_default_role
self.role ||= :batman
end
I am stuck on the test below and not sure how to go about checking the if the role was successfully attached. Also is there a way to check if the user role can be changed for a user? For example if the user was created with the role of batman, it can be switch to superman?
RSpec.describe User, type: :model do
before do
#user = FactoryBot.create(:user)
end
describe "creation" do
it "can be created" do
expect(#user).to be_valid
end
end
end
You can write expectations for model fields too. Code is pretty self-explanatory:
let(:user){ create(:user) }
it "has role batman" do
expect(user.role).to eq("batman")
end
For changing:
it "changes role" do
expect{
do_something_with(user)
}.to change{ user.reload.role }.from("batman").to("superman")
end
reload might be not needed in model tests, but usually is for other (request/system/etc) where record can change in db but not in exact instance in memory.
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'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.
I have the following code in the controller:
# guest to user sign up view. Method that prepares a guest to become a user by emptying it's generic
#e-mail address.
def guest_signup
if !current_user.guest
redirect_to root_url
end
#user = current_user
#user.email = ""
end
This controller just makes sure that the outcome (a form) doesn't have a generic e-mail address in an input field that the user gets assigned when he is using the application as guest.
I am trying to write an rspec test for it and I have no idea how to properly do it... I know this may sound like development-driven testing rather than the opposite but I need an idea.
Currently I have this that doesn't work:
require 'spec_helper'
describe UsersController do
describe "Guest Signup" do
it "should prepare guest with random e-mail user for signup form, emptying the e-mail" do
current_user = User.create(:email => "guest_#{Time.now.to_i}#{rand(99)}#example.com", :password => "#{Time.now.to_i}#{rand(99999999)}", :guest => true)
get :guest_signup, :user => #user.id
expect(#user.email).to eq ('')
end
end
end
How is #user assigned here? Presumably after the guest_signup method is called. Since #user is referenced in the call to guest_signup, you have an order of operations problem here.
Maybe you should be calling:
get :guest_signup, :user => current_user.id
describe UsersController do
describe 'Guest Signup' do
let(:user) { mock.as_null_object }
before(:each) { controller.stub(:current_user) { user } }
context 'when guest does not exist' do
before(:each) { user.stub(:guest) { false } }
it 'redirects to root path' do
get :guest_signup
response.should redirect_to root_path
end
end
context 'when guest exists' do
before(:each) { user.stub(:guest) { true } }
it 'should prepare guest with random e-mail user for signup form, emptying the e-mail' do
get :guest_signup
assigns(#user).should == user
assigns(#email).should be_empty
end
end
end
end
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