I am trying to test a CanCan ability in my app that also uses Authlogic. I have verified the correct behavior works when using the actual site, but I want to write a functional test that will alert me if this behavior breaks in the future. My ability file is simple, and looks as follows:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
can :read, User
can :manage, User, :id => user.id
cannot :create, User
can :destroy, UserSession
if user.role? :guest
can :create, UserSession
cannot :destroy UserSession
end
end
end
My test for the UserSessionsController is also simple, and looks like this:
test "should redirect new for member" do
default_user = login :default_user
assert default_user.role? :member
assert_raise(CanCan::AccessDenied) { get :new }
assert_redirected_to root_path
end
Just for reference, my test_helper.rb looks like this:
ENV["RAILS_ENV"] = "test"
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
require 'authlogic/test_case'
class ActiveSupport::TestCase
fixtures :all
setup :activate_authlogic
def login(user_login)
UserSession.create users(user_login)
users(user_login)
end
end
When I run my code, my test fails, however:
test_should_redirect_new_for_member FAIL
CanCan::AccessDenied expected but nothing was raised.
Assertion at test/functional/user_sessions_controller_test.rb:13:in `block in <class:UserSessionsControllerTest>'
If I comment out the assert_raise, the redirect assertion also fails. Does anyone see anything wrong with my code that is causing this test to fail?
The problem was that I was rescuing the AccessDenied in my ApplicationController, so the exception was never being raised.
You need to block new action too.
if !(user.role? :member)
can :new, User
end
May be User having 'member' role have access to new action(display form for user) and restricted access to create action.
And one more thing, we don't need to use
cannot [:any_action], [Model]
We can do everything by can itself.
Related
Rails 7 application using devise for authentication.
fixture (model tests pass for all classes):
one:
email: 'me#mail.co'
encrypted_password: <%= User.new.send(:password_digest, '12345678')
test_helper which attempts to re-factor the login and action process into short one-liners (test_access) in the actual controller tests
require 'simplecov'
SimpleCov.start 'rails'
ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"
require 'webmock/minitest'
require 'barby/outputter/png_outputter'
require 'barby/barcode/ean_13'
class ActiveSupport::TestCase
fixtures :all
include Devise::Test::IntegrationHelpers
include Warden::Test::Helpers
parallelize(workers: :number_of_processors)
def log_in(user)
if integration_test?
login_as(user, :scope => :user)
else
sign_in(user)
end
end
class Minitest::Test
def setup
#one = users(:one)
end
end
private
def test_access(user, action)
get '/users/sign_in'
puts user.inspect
sign_in user
# puts user_signed_in?
# puts current_user.manager?
puts user.manager?
post user_session_url
follow_redirect!
puts action
get action
assert_response :success
end
and the controller_test (presently on an intermediate refactoring level, as the method should extend to an array of users)
require "test_helper"
class LokationsControllerTest < ActionDispatch::IntegrationTest
include Devise::Test::IntegrationHelpers
setup do
#users_allowed = [ #one ]
#actions = [dummy_lokations_url]
end
test "should get dummy" do
#actions.each do |action|
test_access(#one, action)
assert_response :success
end
end
the problem is there seems to be no way of verifying whether the user is signed in or not, notwithstanding the inclusion of devise integration helpers. The output in the console points to proper values, but the result is the opposite of the method that should be applied
#<User id: 760812109, email: [...]>
true
http://www.example.com/lokations/dummy
[...]
Expected response to be a <2XX: success>, but was a <302: Found> redirect to <http://www.example.com/>
Response body: <html><body>You are being redirected.</body></html>
because the before_action method applied on the action specifies
def index_manager
if user_signed_in? && current_user.manager?
else
redirect_to root_path and return
end
end
Where is the above mistaken ? (I have a nagging doubt that something may be missing as this pattern has been successfully followed in the past)
Sorry but this is just not a good/acceptable refactor.
Do not set the password digest - ever. That should only be known by the underlying system that encrypts the password. Your code and tests should not even know that it exists. Your code should only ever set the password attribute with a cleartext.
These are integration and not controller tests. Controller tests are subclasses of ActionController::TestCase used in legacy applications. They should not be used. While this might seem like nickpicking you're only confusing yourself and others by conflating them.
Avoid monkeypatching a bunch of junk into ActiveSupport::TestCase or even worse Minitest::Test. If you MUST monkeypatch then write your methods in a module and include it so that the stack trace points there. But do it at the correct level in the class heirarchy. For example separate between the helpers for integration and system tests and include them into ActionDispatch::IntegrationTest and ActionDispatch::SystemTest instead of polluting all your tests.
If you need to add a lot of additional behavior don't reopen the class. Instead create your own test class which inherits from ActionDispatch::IntegrationTest and have your tests subclass it.
If you have code that you want to reuse in multiple tests like the test setup use modules that can be included where they are actually needed.
test_access doesn't belong in the shared subclass way up the tree. This belongs in an actual integration test which covers your authentication system flows or that a specific resource is authorized correctly.
Devise is an authentication system (who is the user?) - but what you're doing here is authorization (who can do what?). Devise doesn't provide any kind of authorization besides preventing access for users that are not authenticated. Neither should it (see SRP).
The index_manager method isn't a good way to implement authorization. If you want to reinvent the wheel make sure you raise an exception and use rescue_from so that the callback chain is halted and so that you're not repeating yourself. You may want to look into existing systems like CanCanCan and Pundit.
What you actually want here is something more like:
class AuthorizationError < StandardError
end
class ApplicationController
before_action :authenticate_user! # use an opt-out secure by default setup.
rescue_from AuthorizationError, with: :deny_access
private
def authorize_manager!
unless current_user.manager?
raise AuthorizationError.new("You need to be a manager to access this resource")
end
end
def deny_access(exception)
redirect_to root_path, error: exception.message
end
end
module AuthorizationIntegrationHelpers
def assert_denies_access
follow_redirect!
assert_current_path root_path
# ...
end
end
class FoosFlowTest < ActionDispatch::IntegrationTest
include AuthorizationIntegrationHelpers
include Warden::Test::Helpers
test "should not be accessable to non-managers" do
login_as(users(:one))
get '/foos'
assert_denies_access
end
end
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 have a controller that depends on the user being authenticated. So it looks like this
class PlansController < ApplicationController
before_action :authenticate_user!
def create
puts "here"
if user_signed_in?
puts "true"
else
puts "false"
end
end
end
My controller tests are working just fine when teh user IS signed in, i.e., when I'm writing something like this:
require 'rails_helper'
require 'devise'
RSpec.configure do |config|
config.include Devise::TestHelpers, :type => :controller
end
describe "create action" do
before do
#user = User.create(...)
sign_in :user, #user
end
it "should puts here and then true" do
post :create
# => here
# => true
end
end
But I'd also like to test what happens in the else statement. Not sure how to do this, it fundamentally doesn't even put the here. Is it possible to test this? Or should I just leave and let Devise be?
describe "create action" do
before do
#user = User.create(...)
# do not sign in user (note I have also tried to do a sign_in and then sign_out, same result)
end
it "should puts here and then true" do
post :create
# => nothing is put, not even the first here!
# => no real "error" either, just a test failure
end
end
The before_action :authenticate_user! will immediately redirect you to the default sign-in page, if the user isn't signed in, skipping the create action altogether.
The if user_signed_in? statement is moot in this case, because the user will always be signed in when that code has the chance to run.
If plans can be created with or without an authenticated user, remove the before_action line.
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