diff --git a/lib/cancan/ability.rb b/lib/cancan/ability.rb index 6dfa175..15efb94 100644 --- a/lib/cancan/ability.rb +++ b/lib/cancan/ability.rb @@ -8,7 +8,7 @@ module CanCan # # def initialize(user) # if user.admin? - # can :manage, :all + # can :access, :all # else # can :read, :all # end @@ -78,11 +78,11 @@ module CanCan # # can [:update, :destroy], [Article, Comment] # - # You can pass :all to match any object and :manage to match any action. Here are some examples. + # You can pass :all to match any object and :access to match any action. Here are some examples. # - # can :manage, :all + # can :access, :all # can :update, :all - # can :manage, Project + # can :access, Project # # You can pass a hash of conditions as the third argument. Here the user can only see active projects which he owns. # @@ -158,11 +158,6 @@ module CanCan # can :update, Comment # can? :modify, Comment # => false # - # Unless that exact alias is used. - # - # can :modify, Comment - # can? :modify, Comment # => true - # # The following aliases are added by default for conveniently mapping common controller actions. # # alias_action :index, :show, :to => :read @@ -172,18 +167,42 @@ module CanCan # This way one can use params[:action] in the controller to determine the permission. def alias_action(*args) target = args.pop[:to] - aliased_actions[target] ||= [] - aliased_actions[target] += args + aliases[:actions][target] ||= [] + aliases[:actions][target] += args end - # Returns a hash of aliased actions. The key is the target and the value is an array of actions aliasing the key. - def aliased_actions - @aliased_actions ||= default_alias_actions + # Alias one or more subjects into another one. + # + # alias_subject :admins, :moderators, :to => :users + # can :update, :users + # + # Then :modify permission will apply to both :update and :destroy requests. + # + # can? :update, :admins # => true + # can? :update, :moderators # => true + # + # This only works in one direction. Passing the aliased subject into the "can?" call + # will not work because aliases are meant to generate more generic subjects. + # + # alias_subject :admins, :moderators, :to => :users + # can :update, :admins + # can? :update, :users # => false + # + def alias_subject(*args) + target = args.pop[:to] + aliases[:subjects][target] ||= [] + aliases[:subjects][target] += args end - # Removes previously aliased actions including the defaults. - def clear_aliased_actions - @aliased_actions = {} + # Returns a hash of action and subject aliases. + def aliases + @aliases ||= default_aliases + end + + # Removes previously aliased actions or subjects including the defaults. + def clear_aliases + aliases[:actions] = {} + aliases[:subjects] = {} end def model_adapter(model_class, action) @@ -206,7 +225,7 @@ module CanCan def unauthorized_message(action, subject) keys = unauthorized_message_keys(action, subject) variables = {:action => action.to_s} - variables[:subject] = (subject.class == Class ? subject : subject.class).to_s.underscore.humanize.downcase + variables[:subject] = (subject.kind_of?(Symbol) ? subject.to_s : subject.class.to_s.underscore.humanize.downcase.pluralize) message = I18n.translate(nil, variables.merge(:scope => :unauthorized, :default => keys + [""])) message.blank? ? nil : message end @@ -230,9 +249,9 @@ module CanCan private def unauthorized_message_keys(action, subject) - subject = (subject.class == Class ? subject : subject.class).name.underscore unless subject.kind_of? Symbol - [subject, :all].map do |try_subject| - [aliases_for_action(action), :manage].flatten.map do |try_action| + subject = (subject.kind_of?(Symbol) ? subject.to_s : subject.class.to_s.underscore.humanize.downcase.pluralize) + [aliases_for(:subjects, subject.to_sym), :all].flatten.map do |try_subject| + [aliases_for(:actions, action.to_sym), :access].flatten.map do |try_action| :"#{try_action}.#{try_subject}" end end.flatten @@ -241,18 +260,18 @@ module CanCan # Accepts an array of actions and returns an array of actions which match. # This should be called before "matches?" and other checking methods since they # rely on the actions to be expanded. - def expand_actions(actions) - actions.map do |action| - aliased_actions[action] ? [action, *expand_actions(aliased_actions[action])] : action + def expand_aliases(type, items) + items.map do |item| + aliases[type][item] ? [item, *expand_aliases(type, aliases[type][item])] : item end.flatten end # Given an action, it will try to find all of the actions which are aliased to it. - # This does the opposite kind of lookup as expand_actions. - def aliases_for_action(action) + # This does the opposite kind of lookup as expand_aliases. + def aliases_for(type, action) results = [action] - aliased_actions.each do |aliased_action, actions| - results += aliases_for_action(aliased_action) if actions.include? action + aliases[type].each do |aliased_action, actions| + results += aliases_for(type, aliased_action) if actions.include? action end results end @@ -265,7 +284,8 @@ module CanCan # This does not take into consideration any hash conditions or block statements def relevant_rules(action, subject) rules.reverse.select do |rule| - rule.expanded_actions = expand_actions(rule.actions) + rule.expanded_actions = expand_aliases(:actions, rule.actions) + rule.expanded_subjects = expand_aliases(:subjects, rule.subjects) rule.relevant? action, subject end end @@ -286,11 +306,15 @@ module CanCan end end - def default_alias_actions + def default_aliases { - :read => [:index, :show], - :create => [:new], - :update => [:edit], + :subjects => {}, + :actions => { + :read => [:index, :show], + :create => [:new], + :update => [:edit], + :destroy => [:delete], + } } end end diff --git a/lib/cancan/rule.rb b/lib/cancan/rule.rb index 2ab2b01..ee60705 100644 --- a/lib/cancan/rule.rb +++ b/lib/cancan/rule.rb @@ -4,7 +4,7 @@ module CanCan # helpful methods to determine permission checking and conditions hash generation. class Rule # :nodoc: attr_reader :base_behavior, :subjects, :actions, :conditions - attr_writer :expanded_actions + attr_writer :expanded_actions, :expanded_subjects # The first argument when initializing is the base_behavior which is a true/false # value. True for "can" and false for "cannot". The next two arguments are the action @@ -30,11 +30,11 @@ module CanCan def matches_conditions?(action, subject, extra_args) if @match_all call_block_with_all(action, subject, extra_args) - elsif @block && !subject_class?(subject) + elsif @block && subject_object?(subject) @block.call(subject, *extra_args) elsif @conditions.kind_of?(Hash) && subject.class == Hash nested_subject_matches_conditions?(subject) - elsif @conditions.kind_of?(Hash) && !subject_class?(subject) + elsif @conditions.kind_of?(Hash) && subject_object?(subject) matches_conditions_hash?(subject) else # Don't stop at "cannot" definitions when there are conditions. @@ -72,21 +72,24 @@ module CanCan private - def subject_class?(subject) - klass = (subject.kind_of?(Hash) ? subject.values.first : subject).class - klass == Class || klass == Module + def subject_object?(subject) + # klass = (subject.kind_of?(Hash) ? subject.values.first : subject).class + # klass == Class || klass == Module + !subject.kind_of?(Symbol) end def matches_action?(action) - @expanded_actions.include?(:manage) || @expanded_actions.include?(action) + @expanded_actions.include?(:access) || @expanded_actions.include?(action) end def matches_subject?(subject) - @subjects.include?(:all) || @subjects.include?(subject) || matches_subject_class?(subject) + subject = subject_name(subject) if subject_object? subject + @expanded_subjects.include?(:all) || @expanded_subjects.include?(subject) # || matches_subject_class?(subject) end + # TODO deperecate this def matches_subject_class?(subject) - @subjects.any? { |sub| sub.kind_of?(Module) && (subject.kind_of?(sub) || subject.class.to_s == sub.to_s || subject.kind_of?(Module) && subject.ancestors.include?(sub)) } + @expanded_subjects.any? { |sub| sub.kind_of?(Module) && (subject.kind_of?(sub) || subject.class.to_s == sub.to_s || subject.kind_of?(Module) && subject.ancestors.include?(sub)) } end # Checks if the given subject matches the given conditions hash. @@ -128,15 +131,19 @@ module CanCan end def call_block_with_all(action, subject, extra_args) - if subject.class == Class - @block.call(action, subject, nil, *extra_args) + if subject_object? subject + @block.call(action, subject_name(subject), subject, *extra_args) else - @block.call(action, subject.class, subject, *extra_args) + @block.call(action, subject, nil, *extra_args) end end + def subject_name(subject) + subject.class.to_s.underscore.humanize.downcase.pluralize.to_sym + end + def model_adapter(subject) - ModelAdapters::AbstractAdapter.adapter_class(subject_class?(subject) ? subject : subject.class) + ModelAdapters::AbstractAdapter.adapter_class(subject_object?(subject) ? subject.class : subject) end end end diff --git a/spec/cancan/ability_spec.rb b/spec/cancan/ability_spec.rb index 84e2216..9ae0290 100644 --- a/spec/cancan/ability_spec.rb +++ b/spec/cancan/ability_spec.rb @@ -6,341 +6,255 @@ describe CanCan::Ability do @ability.extend(CanCan::Ability) end - it "should be able to :read anything" do - @ability.can :read, :all - @ability.can?(:read, String).should be_true - @ability.can?(:read, 123).should be_true + + # Basic Action & Subject + + it "allows access to only what is defined" do + @ability.can?(:paint, :fences).should be_false + @ability.can :paint, :fences + @ability.can?(:paint, :fences).should be_true + @ability.can?(:wax, :fences).should be_false + @ability.can?(:paint, :cars).should be_false end - it "should not have permission to do something it doesn't know about" do - @ability.can?(:foodfight, String).should be_false + it "allows access to everything when :access, :all is used" do + @ability.can?(:paint, :fences).should be_false + @ability.can :access, :all + @ability.can?(:paint, :fences).should be_true + @ability.can?(:wax, :fences).should be_true + @ability.can?(:paint, :cars).should be_true end - it "should pass true to `can?` when non false/nil is returned in block" do - @ability.can :read, :all - @ability.can :read, Symbol do |sym| - "foo" # TODO test that sym is nil when no instance is passed - end - @ability.can?(:read, :some_symbol).should == true + it "allows access to multiple actions and subjects" do + @ability.can [:paint, :sand], [:fences, :decks] + @ability.can?(:paint, :fences).should be_true + @ability.can?(:sand, :fences).should be_true + @ability.can?(:paint, :decks).should be_true + @ability.can?(:sand, :decks).should be_true + @ability.can?(:wax, :fences).should be_false + @ability.can?(:paint, :cars).should be_false end - it "should pass nil to a block when no instance is passed" do - @ability.can :read, Symbol do |sym| - sym.should be_nil - true - end - @ability.can?(:read, Symbol).should be_true + + # Aliases + + it "has default index, show, new, update, delete aliases" do + @ability.can :read, :projects + @ability.can?(:index, :projects).should be_true + @ability.can?(:show, :projects).should be_true + @ability.can :create, :projects + @ability.can?(:new, :projects).should be_true + @ability.can :update, :projects + @ability.can?(:edit, :projects).should be_true + @ability.can :destroy, :projects + @ability.can?(:delete, :projects).should be_true end - it "should pass to previous rule, if block returns false or nil" do - @ability.can :read, Symbol - @ability.can :read, Integer do |i| - i < 5 - end - @ability.can :read, Integer do |i| - i > 10 - end - @ability.can?(:read, Symbol).should be_true - @ability.can?(:read, 11).should be_true - @ability.can?(:read, 1).should be_true - @ability.can?(:read, 6).should be_false - end - - it "should not pass class with object if :all objects are accepted" do - @ability.can :preview, :all do |object| - object.should == 123 - @block_called = true - end - @ability.can?(:preview, 123) - @block_called.should be_true - end - - it "should not call block when only class is passed, only return true" do - @block_called = false - @ability.can :preview, :all do |object| - @block_called = true - end - @ability.can?(:preview, Hash).should be_true - @block_called.should be_false - end - - it "should pass only object for global manage actions" do - @ability.can :manage, String do |object| - object.should == "foo" - @block_called = true - end - @ability.can?(:stuff, "foo").should - @block_called.should be_true - end - - it "should alias update or destroy actions to modify action" do + it "follows deep action aliases" do @ability.alias_action :update, :destroy, :to => :modify - @ability.can :modify, :all - @ability.can?(:update, 123).should be_true - @ability.can?(:destroy, 123).should be_true + @ability.can :modify, :projects + @ability.can?(:update, :projects).should be_true + @ability.can?(:destroy, :projects).should be_true + @ability.can?(:edit, :projects).should be_true end - it "should allow deeply nested aliased actions" do - @ability.alias_action :increment, :to => :sort - @ability.alias_action :sort, :to => :modify - @ability.can :modify, :all - @ability.can?(:increment, 123).should be_true - end - - it "should always call block with arguments when passing no arguments to can" do - @ability.can do |action, object_class, object| - action.should == :foo - object_class.should == 123.class - object.should == 123 - @block_called = true - end - @ability.can?(:foo, 123) - @block_called.should be_true - end - - it "should pass nil to object when comparing class with can check" do - @ability.can do |action, object_class, object| - action.should == :foo - object_class.should == Hash - object.should be_nil - @block_called = true - end - @ability.can?(:foo, Hash) - @block_called.should be_true - end - - it "should automatically alias index and show into read calls" do - @ability.can :read, :all - @ability.can?(:index, 123).should be_true - @ability.can?(:show, 123).should be_true - end - - it "should automatically alias new and edit into create and update respectively" do - @ability.can :create, :all - @ability.can :update, :all - @ability.can?(:new, 123).should be_true - @ability.can?(:edit, 123).should be_true - end - - it "should not respond to prepare (now using initialize)" do - @ability.should_not respond_to(:prepare) - end - - it "should offer cannot? method which is simply invert of can?" do - @ability.cannot?(:tie, String).should be_true - end - - it "should be able to specify multiple actions and match any" do - @ability.can [:read, :update], :all - @ability.can?(:read, 123).should be_true - @ability.can?(:update, 123).should be_true - @ability.can?(:count, 123).should be_false - end - - it "should be able to specify multiple classes and match any" do - @ability.can :update, [String, Range] - @ability.can?(:update, "foo").should be_true - @ability.can?(:update, 1..3).should be_true - @ability.can?(:update, 123).should be_false - end - - it "should support custom objects in the rule" do - @ability.can :read, :stats - @ability.can?(:read, :stats).should be_true - @ability.can?(:update, :stats).should be_false - @ability.can?(:read, :nonstats).should be_false - end - - it "should check ancestors of class" do - @ability.can :read, Numeric - @ability.can?(:read, Integer).should be_true - @ability.can?(:read, 1.23).should be_true - @ability.can?(:read, "foo").should be_false - end - - it "should support 'cannot' method to define what user cannot do" do - @ability.can :read, :all - @ability.cannot :read, Integer - @ability.can?(:read, "foo").should be_true - @ability.can?(:read, 123).should be_false - end - - it "should pass to previous rule, if block returns false or nil" do - @ability.can :read, :all - @ability.cannot :read, Integer do |int| - int > 10 ? nil : ( int > 5 ) - end - @ability.can?(:read, "foo").should be_true - @ability.can?(:read, 3).should be_true - @ability.can?(:read, 8).should be_false - @ability.can?(:read, 123).should be_true - end - - it "should always return `false` for single cannot definition" do - @ability.cannot :read, Integer do |int| - int > 10 ? nil : ( int > 5 ) - end - @ability.can?(:read, "foo").should be_false - @ability.can?(:read, 3).should be_false - @ability.can?(:read, 8).should be_false - @ability.can?(:read, 123).should be_false - end - - it "should pass to previous cannot definition, if block returns false or nil" do - @ability.cannot :read, :all - @ability.can :read, Integer do |int| - int > 10 ? nil : ( int > 5 ) - end - @ability.can?(:read, "foo").should be_false - @ability.can?(:read, 3).should be_false - @ability.can?(:read, 10).should be_true - @ability.can?(:read, 123).should be_false - end - - it "should append aliased actions" do + it "adds up action aliases" do @ability.alias_action :update, :to => :modify @ability.alias_action :destroy, :to => :modify - @ability.aliased_actions[:modify].should == [:update, :destroy] + @ability.can :modify, :projects + @ability.can?(:update, :projects).should be_true + @ability.can?(:destroy, :projects).should be_true end - it "should clear aliased actions" do - @ability.alias_action :update, :to => :modify - @ability.clear_aliased_actions - @ability.aliased_actions[:modify].should be_nil + it "follows deep subject aliases" do + @ability.alias_subject :mammals, :to => :animals + @ability.alias_subject :cats, :to => :mammals + @ability.can :pet, :animals + @ability.can?(:pet, :mammals).should be_true end - it "should pass additional arguments to block from can?" do - @ability.can :read, Integer do |int, x| - int > x - end - @ability.can?(:read, 2, 1).should be_true - @ability.can?(:read, 2, 3).should be_false + it "clears current and default aliases" do + @ability.alias_action :update, :destroy, :to => :modify + @ability.clear_aliases + @ability.can :modify, :projects + @ability.can?(:update, :projects).should be_false + @ability.can :read, :projects + @ability.can?(:show, :projects).should be_false end - it "should use conditions as third parameter and determine abilities from it" do - @ability.can :read, Range, :begin => 1, :end => 3 + + # Hash Conditions + + it "maps object to pluralized subject name" do + @ability.can :read, :ranges + @ability.can?(:read, :ranges).should be_true @ability.can?(:read, 1..3).should be_true - @ability.can?(:read, 1..4).should be_false - @ability.can?(:read, Range).should be_true + @ability.can?(:read, 123).should be_false end - it "should allow an array of options in conditions hash" do - @ability.can :read, Range, :begin => [1, 3, 5] + it "checks conditions hash on instances only" do + @ability.can :read, :ranges, :begin => 1 + @ability.can?(:read, :ranges).should be_true + @ability.can?(:read, 1..3).should be_true + @ability.can?(:read, 2..4).should be_false + end + + it "checks conditions on both rules and matches either one" do + @ability.can :read, :ranges, :begin => 1 + @ability.can :read, :ranges, :begin => 2 + @ability.can?(:read, 1..3).should be_true + @ability.can?(:read, 2..4).should be_true + @ability.can?(:read, 3..5).should be_false + end + + it "checks an array of options in conditions hash" do + @ability.can :read, :ranges, :begin => [1, 3, 5] @ability.can?(:read, 1..3).should be_true @ability.can?(:read, 2..4).should be_false @ability.can?(:read, 3..5).should be_true end - it "should allow a range of options in conditions hash" do - @ability.can :read, Range, :begin => 1..3 + it "checks a range of options in conditions hash" do + @ability.can :read, :ranges, :begin => 1..3 @ability.can?(:read, 1..10).should be_true @ability.can?(:read, 3..30).should be_true @ability.can?(:read, 4..40).should be_false end - it "should allow nested hashes in conditions hash" do - @ability.can :read, Range, :begin => { :to_i => 5 } + it "checks nested conditions hash" do + @ability.can :read, :ranges, :begin => { :to_i => 5 } @ability.can?(:read, 5..7).should be_true @ability.can?(:read, 6..8).should be_false end - it "should match any element passed in to nesting if it's an array (for has_many associations)" do - @ability.can :read, Range, :to_a => { :to_i => 3 } + it "matches any element passed in to nesting if it's an array (for has_many associations)" do + @ability.can :read, :ranges, :to_a => { :to_i => 3 } @ability.can?(:read, 1..5).should be_true @ability.can?(:read, 4..6).should be_false end - it "should not stop at cannot definition when comparing class" do - @ability.can :read, Range - @ability.cannot :read, Range, :begin => 1 - @ability.can?(:read, 2..5).should be_true - @ability.can?(:read, 1..5).should be_false - @ability.can?(:read, Range).should be_true - end - it "should stop at cannot definition when no hash is present" do - @ability.can :read, :all - @ability.cannot :read, Range - @ability.can?(:read, 1..5).should be_false - @ability.can?(:read, Range).should be_false - end + # Block Conditions - it "should allow to check ability for Module" do - module B; end - class A; include B; end - @ability.can :read, B - @ability.can?(:read, A).should be_true - @ability.can?(:read, A.new).should be_true - end - - it "should pass nil to a block for ability on Module when no instance is passed" do - module B; end - class A; include B; end - @ability.can :read, B do |sym| - sym.should be_nil - true + it "executes block passing object only when instance is used" do + @ability.can :read, :ranges do |range| + range.begin == 5 end - @ability.can?(:read, B).should be_true - @ability.can?(:read, A).should be_true + @ability.can?(:read, :ranges).should be_true + @ability.can?(:read, 5..7).should be_true + @ability.can?(:read, 6..8).should be_false end - it "passing a hash of subjects should check permissions through association" do - @ability.can :read, Range, :string => {:length => 3} - @ability.can?(:read, "foo" => Range).should be_true - @ability.can?(:read, "foobar" => Range).should be_false - @ability.can?(:read, 123 => Range).should be_true + it "returns true when other object is returned in block" do + @ability.can :read, :ranges do |range| + "foo" + end + @ability.can?(:read, 5..7).should be_true end - - it "should allow to check ability on Hash-like object" do + + it "passes to previous rule when block returns false" do + @ability.can :read, :fixnums do |i| + i < 5 + end + @ability.can :read, :fixnums do |i| + i > 10 + end + @ability.can?(:read, 11).should be_true + @ability.can?(:read, 1).should be_true + @ability.can?(:read, 6).should be_false + end + + it "calls block passing arguments when no arguments are given to can" do + @ability.can do |action, subject, object| + action.should == :read + subject.should == :ranges + object.should == (2..4) + @block_called = true + end + @ability.can?(:read, 2..4) + @block_called.should be_true + end + + it "should raise an error when attempting to use a block with a hash condition since it's not likely what they want" do + lambda { + @ability.can :read, :ranges, :published => true do + false + end + }.should raise_error(CanCan::Error, "You are not able to supply a block with a hash of conditions in read ranges ability. Use either one.") + end + + + # Cannot + + it "offers cannot? method which inverts can?" do + @ability.cannot?(:wax, :cars).should be_true + end + + it "supports 'cannot' method to define what user cannot do" do + @ability.can :read, :all + @ability.cannot :read, :ranges + @ability.can?(:read, :books).should be_true + @ability.can?(:read, 1..3).should be_false + @ability.can?(:read, :ranges).should be_false + end + + it "passes to previous rule if cannot check returns false" do + @ability.can :read, :all + @ability.cannot :read, :ranges, :begin => 3 + @ability.cannot :read, :ranges do |range| + range.begin == 5 + end + @ability.can?(:read, :books).should be_true + @ability.can?(:read, 2..4).should be_true + @ability.can?(:read, 3..7).should be_false + @ability.can?(:read, 5..9).should be_false + end + + + # Hash Association + + it "checks permission through association when hash is passed as subject" do + @ability.can :read, :books, :range => {:begin => 3} + @ability.can?(:read, (1..4) => :books).should be_false + @ability.can?(:read, (3..5) => :books).should be_true + @ability.can?(:read, 123 => :books).should be_true + end + + it "checks ability on hash subclass" do class Container < Hash; end - @ability.can :read, Container + @ability.can :read, :containers @ability.can?(:read, Container.new).should be_true end - it "should have initial attributes based on hash conditions of 'new' action" do - @ability.can :manage, Range, :foo => "foo", :hash => {:skip => "hashes"} - @ability.can :create, Range, :bar => 123, :array => %w[skip arrays] - @ability.can :new, Range, :baz => "baz", :range => 1..3 - @ability.cannot :new, Range, :ignore => "me" - @ability.attributes_for(:new, Range).should == {:foo => "foo", :bar => 123, :baz => "baz"} + + # Initial Attributes + + it "has initial attributes based on hash conditions for a given action" do + @ability.can :access, :ranges, :foo => "foo", :hash => {:skip => "hashes"} + @ability.can :create, :ranges, :bar => 123, :array => %w[skip arrays] + @ability.can :new, :ranges, :baz => "baz", :range => 1..3 + @ability.cannot :new, :ranges, :ignore => "me" + @ability.attributes_for(:new, :ranges).should == {:foo => "foo", :bar => 123, :baz => "baz"} end - it "should raise access denied exception if ability us unauthorized to perform a certain action" do + + # Unauthorized Exception + + it "raises CanCan::AccessDenied when calling authorize! on unauthorized action" do begin - @ability.authorize! :read, :foo, 1, 2, 3, :message => "Access denied!" + @ability.authorize! :read, :books, :message => "Access denied!" rescue CanCan::AccessDenied => e e.message.should == "Access denied!" e.action.should == :read - e.subject.should == :foo + e.subject.should == :books else fail "Expected CanCan::AccessDenied exception to be raised" end end - it "should not raise access denied exception if ability is authorized to perform an action" do - @ability.can :read, :foo - lambda { @ability.authorize!(:read, :foo) }.should_not raise_error - end - - it "should know when block is used in conditions" do - @ability.can :read, :foo - @ability.should_not have_block(:read, :foo) - @ability.can :read, :foo do |foo| - false - end - @ability.should have_block(:read, :foo) - end - - it "should know when raw sql is used in conditions" do - @ability.can :read, :foo - @ability.should_not have_raw_sql(:read, :foo) - @ability.can :read, :foo, 'false' - @ability.should have_raw_sql(:read, :foo) - end - it "should raise access denied exception with default message if not specified" do begin - @ability.authorize! :read, :foo + @ability.authorize! :read, :books rescue CanCan::AccessDenied => e e.default_message = "Access denied!" e.message.should == "Access denied!" @@ -349,7 +263,32 @@ describe CanCan::Ability do end end - it "should determine model adapter class by asking AbstractAdapter" do + it "does not raise access denied exception if ability is authorized to perform an action" do + @ability.can :read, :books + lambda { @ability.authorize!(:read, :books) }.should_not raise_error + end + + + # Determining Conditions + + it "knows when a block is used for conditions" do + @ability.can :read, :books + @ability.should_not have_block(:read, :books) + @ability.can :read, :books do |foo| + false + end + @ability.should have_block(:read, :books) + end + + it "knows when raw sql is used for conditions" do + @ability.can :read, :books + @ability.should_not have_raw_sql(:read, :books) + @ability.can :read, :books, 'false' + @ability.should have_raw_sql(:read, :books) + end + + it "determines model adapter class by asking AbstractAdapter" do + pending model_class = Object.new adapter_class = Object.new stub(CanCan::ModelAdapters::AbstractAdapter).adapter_class(model_class) { adapter_class } @@ -357,54 +296,50 @@ describe CanCan::Ability do @ability.model_adapter(model_class, :read).should == :adapter_instance end - it "should raise an error when attempting to use a block with a hash condition since it's not likely what they want" do - lambda { - @ability.can :read, Array, :published => true do - false - end - }.should raise_error(CanCan::Error, "You are not able to supply a block with a hash of conditions in read Array ability. Use either one.") - end + + # Unauthorized I18n Message describe "unauthorized message" do after(:each) do I18n.backend = nil end - it "should use action/subject in i18n" do - I18n.backend.store_translations :en, :unauthorized => {:update => {:array => "foo"}} - @ability.unauthorized_message(:update, Array).should == "foo" - @ability.unauthorized_message(:update, [1, 2, 3]).should == "foo" + it "uses action/subject in i18n" do + I18n.backend.store_translations :en, :unauthorized => {:update => {:ranges => "update ranges"}} + @ability.unauthorized_message(:update, :ranges).should == "update ranges" + @ability.unauthorized_message(:update, 2..4).should == "update ranges" @ability.unauthorized_message(:update, :missing).should be_nil end - it "should use symbol as subject directly" do + it "uses symbol as subject directly" do I18n.backend.store_translations :en, :unauthorized => {:has => {:cheezburger => "Nom nom nom. I eated it."}} @ability.unauthorized_message(:has, :cheezburger).should == "Nom nom nom. I eated it." end - it "should fall back to 'manage' and 'all'" do + it "falls back to 'access' and 'all'" do I18n.backend.store_translations :en, :unauthorized => { - :manage => {:all => "manage all", :array => "manage array"}, - :update => {:all => "update all", :array => "update array"} + :access => {:all => "access all", :ranges => "access ranges"}, + :update => {:all => "update all", :ranges => "update ranges"} } - @ability.unauthorized_message(:update, Array).should == "update array" - @ability.unauthorized_message(:update, Hash).should == "update all" - @ability.unauthorized_message(:foo, Array).should == "manage array" - @ability.unauthorized_message(:foo, Hash).should == "manage all" + @ability.unauthorized_message(:update, :ranges).should == "update ranges" + @ability.unauthorized_message(:update, :hashes).should == "update all" + @ability.unauthorized_message(:create, :ranges).should == "access ranges" + @ability.unauthorized_message(:create, :hashes).should == "access all" end - it "should follow aliased actions" do - I18n.backend.store_translations :en, :unauthorized => {:modify => {:array => "modify array"}} + it "follows aliases" do + I18n.backend.store_translations :en, :unauthorized => {:modify => {:ranges => "modify ranges"}} @ability.alias_action :update, :to => :modify - @ability.unauthorized_message(:update, Array).should == "modify array" - @ability.unauthorized_message(:edit, Array).should == "modify array" + @ability.alias_subject :areas, :to => :ranges + @ability.unauthorized_message(:update, :areas).should == "modify ranges" + @ability.unauthorized_message(:edit, :ranges).should == "modify ranges" end - it "should have variables for action and subject" do - I18n.backend.store_translations :en, :unauthorized => {:manage => {:all => "%{action} %{subject}"}} # old syntax for now in case testing with old I18n - @ability.unauthorized_message(:update, Array).should == "update array" - @ability.unauthorized_message(:update, ArgumentError).should == "update argument error" - @ability.unauthorized_message(:edit, 1..3).should == "edit range" + it "has variables for action and subject" do + I18n.backend.store_translations :en, :unauthorized => {:access => {:all => "%{action} %{subject}"}} # old syntax for now in case testing with old I18n + @ability.unauthorized_message(:update, :ranges).should == "update ranges" + @ability.unauthorized_message(:edit, 1..3).should == "edit ranges" + # @ability.unauthorized_message(:update, ArgumentError).should == "update argument error" end end end