Skip to content

Instantly share code, notes, and snippets.

@yaronw
Created January 17, 2012 16:47
Show Gist options
  • Save yaronw/1627433 to your computer and use it in GitHub Desktop.
Save yaronw/1627433 to your computer and use it in GitHub Desktop.
An RSpec Macro for Testing That Controller Actions Perform CanCan Authorization
=begin
# This RSpec Macro is for use with the authorization plugin CanCan. It lets you test that a controller checks for a
# specific authorization, and if it fails, that the controller performs a certain action, such as redirecting to a
# different page.
# Example Usages in specs:
it_should_authorize_access_for(:new, Company)
it_should_authorize_access_for(:edit, Company.new) { get :edit, id: @company }
# For more on using it_should_authorize_access_for, see comment block above the definition of it_should_authorize_access_for
# in the code below.
# To set up and configure this macro, first add the line
config.include(CanCanControllerMacros::Base, :type => :controller)
# to the "RSpec.configure do |config|" block.
# Then configure with the following code:
CanCanControllerMacros.configure do
# The following configuration code sets the test for the action that happens when user is unauthorized.
# The block given to is what it_should_authorize_access_for tests for to determine that the proper action is taken when a user is unauthorized.
# The default is { action_proc.should raise_error CanCan::AccessDenied }, and if that's the desired test, no configuration is needed.
# The action_call variable is set up by when_authorized as a lambda that makes the web call.
config.when_unauthorized { action_call.call; response.should do_some_custom_thing }
end
# Examples configure blocks:
CanCanControllerMacros.configure do
config.when_unauthorized { action_proc.should raise_error SomeExceptionClass } # tests that an exception is raised when unauthorized
end
CanCanControllerMacros.configure do
config.when_unauthorized { action_proc.call; response.should redirect_to unauthorized_url } # tests that user is redirected when unauthorized
end
=end
require 'active_support/concern'
module CanCanControllerMacros
# An object that holds configuration vars for CanCanControllerMacros
class Configuration
# Sets and retrieves the action that happens when user is unauthorized.
def when_unauthorized(&block)
if block_given? # set the configuration
@when_unauthorized = block
else # if no block is given, retrieve the configuration
@when_unauthorized || (Proc.new { action_proc.should raise_error CanCan::AccessDenied }) # the right side expression is the default
end
end
end
@@config = CanCanControllerMacros::Configuration.new
mattr_reader :config
# Gives a block configuration style like that of Rails.
# See https://github.com/yaronw/method_cacher/blob/master/lib/method_cacher/base.rb
def self.configure(&block)
if block_given?
if block.arity == 1
yield config
else
instance_eval &block
end
end
end
module Base
extend ActiveSupport::Concern
module ClassMethods
# An RSpec macro that checks a request fails if user is not authorized through CanCan for a given action/object combination.
# Use this macro to test that a controller relies on CanCan for authorizing a specific action/object combination,
# and then use unit tests with the CanCan Ability class to test the conditions that authorize the combination.
#
# @param auth_action [symbol]- The action being authorized.
# @param auth_obj - [Class, Object] - The class or object being authorized.
# @param block - A block that makes the request to the controller's action.
# If omitted, the action defaults to "get auth_action".
#
# Example Usages in specs:
# it_should_authorize_access_for(:new, Company)
# it_should_authorize_access_for(:edit, Company.new) { get :edit, id: @company }
def it_should_authorize_access_for(auth_action, auth_obj, &block)
obj_name = auth_obj.is_a?(Class) ? auth_obj.name + " class" : auth_obj.class.name + " instance"
it "should block unauthorized access if user is unauthorized for #{auth_action.inspect} action on #{obj_name}" do
# stub an ability that authorizes everything except the action on the object/class being tested for
ability = stub_ability
ability.can(:manage, :all)
if auth_obj.is_a? Class
# If auth_obj is a class, set ability to unauthorize everything but the specified action on every object in the class.
ability.cannot(auth_action, auth_obj)
else
# If auth_obj is an instance object,
# the following will ensure that there's authorization for the class but not an instance object of that class.
# This enables us to ensure that the action specifically calls, for example, "authorize! :edit, @company" and not just
# "authorize! :edit, Company"
ability.cannot(auth_action, auth_obj.class)
ability.can(auth_action, auth_obj.class) { false }
end
# creates the procedure that makes a call to the action
self.action_proc = lambda do
if block_given?
instance_eval &block # evaluates the block in the instance context and not in the context in which it was defined
else
get auth_action
end
end
instance_eval &CanCanControllerMacros.config.when_unauthorized # check for desired response when unauthorized
end
end
end
module InstanceMethods
mattr_accessor :action_proc # stores the proc that calls the action
def stub_ability
ability = Object.new
ability.extend(CanCan::Ability)
@controller.stub(:current_ability) { ability }
ability
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment