marnen-cancan 2.0.0.alpha.pre.f1cebde51a87be149b4970a3287826bb63c0ac0b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/CHANGELOG.rdoc +381 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.rdoc +108 -0
- data/Rakefile +18 -0
- data/init.rb +1 -0
- data/lib/cancan.rb +13 -0
- data/lib/cancan/ability.rb +348 -0
- data/lib/cancan/controller_additions.rb +392 -0
- data/lib/cancan/controller_resource.rb +265 -0
- data/lib/cancan/exceptions.rb +53 -0
- data/lib/cancan/inherited_resource.rb +20 -0
- data/lib/cancan/matchers.rb +14 -0
- data/lib/cancan/model_adapters/abstract_adapter.rb +56 -0
- data/lib/cancan/model_adapters/active_record_adapter.rb +172 -0
- data/lib/cancan/model_adapters/data_mapper_adapter.rb +34 -0
- data/lib/cancan/model_adapters/default_adapter.rb +7 -0
- data/lib/cancan/model_adapters/mongoid_adapter.rb +54 -0
- data/lib/cancan/model_additions.rb +29 -0
- data/lib/cancan/rule.rb +178 -0
- data/lib/generators/cancan/ability/USAGE +5 -0
- data/lib/generators/cancan/ability/ability_generator.rb +16 -0
- data/lib/generators/cancan/ability/templates/ability.rb +24 -0
- data/lib/generators/cancan/ability/templates/ability_spec.rb +16 -0
- data/lib/generators/cancan/ability/templates/ability_test.rb +10 -0
- data/spec/README.rdoc +28 -0
- data/spec/cancan/ability_spec.rb +541 -0
- data/spec/cancan/controller_additions_spec.rb +118 -0
- data/spec/cancan/controller_resource_spec.rb +535 -0
- data/spec/cancan/exceptions_spec.rb +58 -0
- data/spec/cancan/inherited_resource_spec.rb +58 -0
- data/spec/cancan/matchers_spec.rb +33 -0
- data/spec/cancan/model_adapters/active_record_adapter_spec.rb +278 -0
- data/spec/cancan/model_adapters/data_mapper_adapter_spec.rb +120 -0
- data/spec/cancan/model_adapters/default_adapter_spec.rb +7 -0
- data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +227 -0
- data/spec/cancan/rule_spec.rb +55 -0
- data/spec/matchers.rb +13 -0
- data/spec/spec_helper.rb +49 -0
- metadata +197 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
module CanCan
|
2
|
+
module ModelAdapters
|
3
|
+
class DataMapperAdapter < AbstractAdapter
|
4
|
+
def self.for_class?(model_class)
|
5
|
+
model_class <= DataMapper::Resource
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.find(model_class, id)
|
9
|
+
model_class.get(id)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.override_conditions_hash_matching?(subject, conditions)
|
13
|
+
conditions.any? { |k,v| !k.kind_of?(Symbol) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.matches_conditions_hash?(subject, conditions)
|
17
|
+
collection = DataMapper::Collection.new(subject.query, [ subject ])
|
18
|
+
!!collection.first(conditions)
|
19
|
+
end
|
20
|
+
|
21
|
+
def database_records
|
22
|
+
scope = @model_class.all(:conditions => ["0 = 1"])
|
23
|
+
cans, cannots = @rules.partition { |r| r.base_behavior }
|
24
|
+
return scope if cans.empty?
|
25
|
+
# apply unions first, then differences. this mean cannot overrides can
|
26
|
+
cans.each { |r| scope += @model_class.all(:conditions => r.conditions) }
|
27
|
+
cannots.each { |r| scope -= @model_class.all(:conditions => r.conditions) }
|
28
|
+
scope
|
29
|
+
end
|
30
|
+
end # class DataMapper
|
31
|
+
end # module ModelAdapters
|
32
|
+
end # module CanCan
|
33
|
+
|
34
|
+
DataMapper::Model.append_extensions(CanCan::ModelAdditions::ClassMethods)
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module CanCan
|
2
|
+
module ModelAdapters
|
3
|
+
class MongoidAdapter < AbstractAdapter
|
4
|
+
def self.for_class?(model_class)
|
5
|
+
model_class <= Mongoid::Document
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.override_conditions_hash_matching?(subject, conditions)
|
9
|
+
conditions.any? do |k,v|
|
10
|
+
key_is_not_symbol = lambda { !k.kind_of?(Symbol) }
|
11
|
+
subject_value_is_array = lambda do
|
12
|
+
subject.respond_to?(k) && subject.send(k).is_a?(Array)
|
13
|
+
end
|
14
|
+
|
15
|
+
key_is_not_symbol.call || subject_value_is_array.call
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.matches_conditions_hash?(subject, conditions)
|
20
|
+
# To avoid hitting the db, retrieve the raw Mongo selector from
|
21
|
+
# the Mongoid Criteria and use Mongoid::Matchers#matches?
|
22
|
+
subject.matches?( subject.class.where(conditions).selector )
|
23
|
+
end
|
24
|
+
|
25
|
+
def database_records
|
26
|
+
if @rules.size == 0
|
27
|
+
@model_class.where(:_id => {'$exists' => false, '$type' => 7}) # return no records in Mongoid
|
28
|
+
elsif @rules.size == 1 && @rules[0].conditions.is_a?(Mongoid::Criteria)
|
29
|
+
@rules[0].conditions
|
30
|
+
else
|
31
|
+
# we only need to process can rules if
|
32
|
+
# there are no rules with empty conditions
|
33
|
+
rules = @rules.reject { |rule| rule.conditions.empty? && rule.base_behavior }
|
34
|
+
process_can_rules = @rules.count == rules.count
|
35
|
+
|
36
|
+
rules.inject(@model_class.all) do |records, rule|
|
37
|
+
if process_can_rules && rule.base_behavior
|
38
|
+
records.or rule.conditions
|
39
|
+
elsif !rule.base_behavior
|
40
|
+
records.excludes rule.conditions
|
41
|
+
else
|
42
|
+
records
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# simplest way to add `accessible_by` to all Mongoid Documents
|
52
|
+
module Mongoid::Document::ClassMethods
|
53
|
+
include CanCan::ModelAdditions::ClassMethods
|
54
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module CanCan
|
2
|
+
|
3
|
+
# This module adds the accessible_by class method to a model. It is included in the model adapters.
|
4
|
+
module ModelAdditions
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
# Returns a scope which fetches only the records that the passed ability
|
9
|
+
# can perform a given action on. The action defaults to :index. This
|
10
|
+
# is usually called from a controller and passed the +current_ability+.
|
11
|
+
#
|
12
|
+
# @articles = Article.accessible_by(current_ability)
|
13
|
+
#
|
14
|
+
# Here only the articles which the user is able to read will be returned.
|
15
|
+
# If the user does not have permission to read any articles then an empty
|
16
|
+
# result is returned. Since this is a scope it can be combined with any
|
17
|
+
# other scopes or pagination.
|
18
|
+
#
|
19
|
+
# An alternative action can optionally be passed as a second argument.
|
20
|
+
#
|
21
|
+
# @articles = Article.accessible_by(current_ability, :update)
|
22
|
+
#
|
23
|
+
# Here only the articles which the user can update are returned.
|
24
|
+
def accessible_by(ability, action = :index)
|
25
|
+
ability.model_adapter(self, action).database_records
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/cancan/rule.rb
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
module CanCan
|
2
|
+
# This class is used internally and should only be called through Ability.
|
3
|
+
# it holds the information about a "can" call made on Ability and provides
|
4
|
+
# helpful methods to determine permission checking and conditions hash generation.
|
5
|
+
class Rule # :nodoc:
|
6
|
+
attr_reader :base_behavior, :subjects, :actions, :conditions
|
7
|
+
attr_writer :expanded_actions, :expanded_subjects
|
8
|
+
|
9
|
+
# The first argument when initializing is the base_behavior which is a true/false
|
10
|
+
# value. True for "can" and false for "cannot". The next two arguments are the action
|
11
|
+
# and subject respectively (such as :read, @project). The third argument is a hash
|
12
|
+
# of conditions and the last one is the block passed to the "can" call.
|
13
|
+
def initialize(base_behavior, action = nil, subject = nil, *extra_args, &block)
|
14
|
+
@match_all = action.nil? && subject.nil?
|
15
|
+
@base_behavior = base_behavior
|
16
|
+
@actions = [action].flatten
|
17
|
+
@subjects = [subject].flatten
|
18
|
+
@attributes = [extra_args.shift].flatten if extra_args.first.kind_of?(Symbol) || extra_args.first.kind_of?(Array) && extra_args.first.first.kind_of?(Symbol)
|
19
|
+
raise Error, "You are not able to supply a block with a hash of conditions in #{action} #{subject} ability. Use either one." if extra_args.first.kind_of?(Hash) && !block.nil?
|
20
|
+
@conditions = extra_args.first || {}
|
21
|
+
@block = block
|
22
|
+
end
|
23
|
+
|
24
|
+
# Matches the subject, action, and given attribute. Conditions are not checked here.
|
25
|
+
def relevant?(action, subject, attribute)
|
26
|
+
subject = subject.values.first if subject.class == Hash
|
27
|
+
@match_all || (matches_action?(action) && matches_subject?(subject) && matches_attribute?(attribute))
|
28
|
+
end
|
29
|
+
|
30
|
+
# Matches the block or conditions hash
|
31
|
+
def matches_conditions?(action, subject, attribute)
|
32
|
+
if @match_all
|
33
|
+
call_block_with_all(action, subject, attribute)
|
34
|
+
elsif @block && subject_object?(subject)
|
35
|
+
@block.arity == 1 ? @block.call(subject) : @block.call(subject, attribute)
|
36
|
+
elsif @conditions.kind_of?(Hash) && subject.class == Hash
|
37
|
+
nested_subject_matches_conditions?(subject)
|
38
|
+
elsif @conditions.kind_of?(Hash) && subject_object?(subject)
|
39
|
+
matches_conditions_hash?(subject)
|
40
|
+
else
|
41
|
+
# Don't stop at "cannot" definitions when there are conditions.
|
42
|
+
@conditions.empty? ? true : @base_behavior
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def only_block?
|
47
|
+
!conditions? && !@block.nil?
|
48
|
+
end
|
49
|
+
|
50
|
+
def only_raw_sql?
|
51
|
+
@block.nil? && conditions? && !@conditions.kind_of?(Hash)
|
52
|
+
end
|
53
|
+
|
54
|
+
def attributes?
|
55
|
+
@attributes.present?
|
56
|
+
end
|
57
|
+
|
58
|
+
def conditions?
|
59
|
+
@conditions.present?
|
60
|
+
end
|
61
|
+
|
62
|
+
def instance_conditions?
|
63
|
+
@block || conditions?
|
64
|
+
end
|
65
|
+
|
66
|
+
def unmergeable?
|
67
|
+
@conditions.respond_to?(:keys) && (! @conditions.keys.first.kind_of? Symbol)
|
68
|
+
end
|
69
|
+
|
70
|
+
def associations_hash(conditions = @conditions)
|
71
|
+
hash = {}
|
72
|
+
conditions.map do |name, value|
|
73
|
+
hash[name] = associations_hash(value) if value.kind_of? Hash
|
74
|
+
end if conditions.kind_of? Hash
|
75
|
+
hash
|
76
|
+
end
|
77
|
+
|
78
|
+
def attributes_from_conditions
|
79
|
+
attributes = {}
|
80
|
+
@conditions.each do |key, value|
|
81
|
+
attributes[key] = value unless [Array, Range, Hash].include? value.class
|
82
|
+
end if @conditions.kind_of? Hash
|
83
|
+
attributes
|
84
|
+
end
|
85
|
+
|
86
|
+
def specificity
|
87
|
+
specificity = 1
|
88
|
+
specificity += 1 if attributes? || conditions?
|
89
|
+
specificity += 2 unless base_behavior
|
90
|
+
specificity
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def subject_object?(subject)
|
96
|
+
# klass = (subject.kind_of?(Hash) ? subject.values.first : subject).class
|
97
|
+
# klass == Class || klass == Module
|
98
|
+
!subject.kind_of?(Symbol) && !subject.kind_of?(String)
|
99
|
+
end
|
100
|
+
|
101
|
+
def matches_action?(action)
|
102
|
+
@expanded_actions.include?(:access) || @expanded_actions.include?(action.to_sym)
|
103
|
+
end
|
104
|
+
|
105
|
+
def matches_subject?(subject)
|
106
|
+
subject = subject_name(subject) if subject_object? subject
|
107
|
+
@expanded_subjects.include?(:all) || @expanded_subjects.include?(subject.to_sym) || @expanded_subjects.include?(subject) # || matches_subject_class?(subject)
|
108
|
+
end
|
109
|
+
|
110
|
+
def matches_attribute?(attribute)
|
111
|
+
# don't consider attributes in a cannot clause when not matching - this can probably be refactored
|
112
|
+
if !@base_behavior && @attributes && attribute.nil?
|
113
|
+
false
|
114
|
+
else
|
115
|
+
@attributes.nil? || attribute.nil? || @attributes.include?(attribute.to_sym)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# TODO deperecate this
|
120
|
+
def matches_subject_class?(subject)
|
121
|
+
@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)) }
|
122
|
+
end
|
123
|
+
|
124
|
+
# Checks if the given subject matches the given conditions hash.
|
125
|
+
# This behavior can be overriden by a model adapter by defining two class methods:
|
126
|
+
# override_matching_for_conditions?(subject, conditions) and
|
127
|
+
# matches_conditions_hash?(subject, conditions)
|
128
|
+
def matches_conditions_hash?(subject, conditions = @conditions)
|
129
|
+
if conditions.empty?
|
130
|
+
true
|
131
|
+
else
|
132
|
+
if model_adapter(subject).override_conditions_hash_matching? subject, conditions
|
133
|
+
model_adapter(subject).matches_conditions_hash? subject, conditions
|
134
|
+
else
|
135
|
+
conditions.all? do |name, value|
|
136
|
+
if model_adapter(subject).override_condition_matching? subject, name, value
|
137
|
+
model_adapter(subject).matches_condition? subject, name, value
|
138
|
+
else
|
139
|
+
attribute = subject.send(name)
|
140
|
+
if value.kind_of?(Hash)
|
141
|
+
if attribute.kind_of? Array
|
142
|
+
attribute.any? { |element| matches_conditions_hash? element, value }
|
143
|
+
else
|
144
|
+
attribute && matches_conditions_hash?(attribute, value)
|
145
|
+
end
|
146
|
+
elsif value.kind_of?(Enumerable)
|
147
|
+
value.include? attribute
|
148
|
+
else
|
149
|
+
attribute == value
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def nested_subject_matches_conditions?(subject_hash)
|
158
|
+
parent, child = subject_hash.first
|
159
|
+
matches_conditions_hash?(parent, @conditions[parent.class.name.downcase.to_sym] || {})
|
160
|
+
end
|
161
|
+
|
162
|
+
def call_block_with_all(action, subject, attribute)
|
163
|
+
if subject_object? subject
|
164
|
+
@block.call(action, subject_name(subject), subject, attribute)
|
165
|
+
else
|
166
|
+
@block.call(action, subject, nil, attribute)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def subject_name(subject)
|
171
|
+
subject.class.to_s.underscore.pluralize.to_sym
|
172
|
+
end
|
173
|
+
|
174
|
+
def model_adapter(subject)
|
175
|
+
CanCan::ModelAdapters::AbstractAdapter.adapter_class(subject_object?(subject) ? subject.class : subject)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Cancan
|
2
|
+
module Generators
|
3
|
+
class AbilityGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path("../templates", __FILE__)
|
5
|
+
|
6
|
+
def generate_ability
|
7
|
+
copy_file "ability.rb", "app/models/ability.rb"
|
8
|
+
if File.exist?(File.join(destination_root, "spec"))
|
9
|
+
copy_file "ability_spec.rb", "spec/models/ability_spec.rb"
|
10
|
+
else
|
11
|
+
copy_file "ability_test.rb", "test/unit/ability_test.rb"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Ability
|
2
|
+
include CanCan::Ability
|
3
|
+
|
4
|
+
def initialize(user)
|
5
|
+
# Define abilities for the passed in (current) user. For example:
|
6
|
+
#
|
7
|
+
# if user
|
8
|
+
# can :access, :all
|
9
|
+
# else
|
10
|
+
# can :access, :home
|
11
|
+
# can :create, [:users, :sessions]
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# Here if there is a user he will be able to perform any action on any controller.
|
15
|
+
# If someone is not logged in he can only access the home, users, and sessions controllers.
|
16
|
+
#
|
17
|
+
# The first argument to `can` is the action the user can perform. The second argument
|
18
|
+
# is the controller name they can perform that action on. You can pass :access and :all
|
19
|
+
# to represent any action and controller respectively. Passing an array to either of
|
20
|
+
# these will grant permission on each item in the array.
|
21
|
+
#
|
22
|
+
# See the wiki for details: https://github.com/ryanb/cancan/wiki/Defining-Abilities
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "cancan/matchers"
|
3
|
+
|
4
|
+
describe Ability do
|
5
|
+
describe "as guest" do
|
6
|
+
before(:each) do
|
7
|
+
@ability = Ability.new(nil)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "can only create a user" do
|
11
|
+
# Define what a guest can and cannot do
|
12
|
+
# @ability.should be_able_to(:create, :users)
|
13
|
+
# @ability.should_not be_able_to(:update, :users)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class AbilityTest < ActiveSupport::TestCase
|
4
|
+
def guest_can_only_create_user
|
5
|
+
ability = Ability.new(nil)
|
6
|
+
# Define what a guest can and cannot do
|
7
|
+
# assert ability.can?(:create, :users)
|
8
|
+
# assert ability.cannot?(:update, :users)
|
9
|
+
end
|
10
|
+
end
|
data/spec/README.rdoc
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
= CanCan Specs
|
2
|
+
|
3
|
+
== Running the specs
|
4
|
+
|
5
|
+
To run the specs first run the +bundle+ command to install the necessary gems and the +rake+ command to run the specs.
|
6
|
+
|
7
|
+
bundle
|
8
|
+
rake
|
9
|
+
|
10
|
+
The specs currently require Ruby 1.8.7. Ruby 1.9.2 support will be coming soon.
|
11
|
+
|
12
|
+
|
13
|
+
== Model Adapters
|
14
|
+
|
15
|
+
CanCan offers separate specs for different model adapters (such as Mongoid and Data Mapper). By default it will use Active Record but you can change this by setting the +MODEL_ADAPTER+ environment variable before running. You can run the +bundle+ command with this as well to ensure you have the installed gems.
|
16
|
+
|
17
|
+
MODEL_ADAPTER=data_mapper bundle
|
18
|
+
MODEL_ADAPTER=data_mapper rake
|
19
|
+
|
20
|
+
The different model adapters you can specify are:
|
21
|
+
|
22
|
+
* active_record (default)
|
23
|
+
* data_mapper
|
24
|
+
* mongoid
|
25
|
+
|
26
|
+
You can also run the +spec_all+ rake task to run specs for each adapter.
|
27
|
+
|
28
|
+
rake spec_all
|
@@ -0,0 +1,541 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe CanCan::Ability do
|
4
|
+
before(:each) do
|
5
|
+
@ability = Object.new
|
6
|
+
@ability.extend(CanCan::Ability)
|
7
|
+
end
|
8
|
+
|
9
|
+
|
10
|
+
# Basic Action & Subject
|
11
|
+
|
12
|
+
it "allows access to only what is defined" do
|
13
|
+
@ability.can?(:paint, :fences).should be_false
|
14
|
+
@ability.can :paint, :fences
|
15
|
+
@ability.can?(:paint, :fences).should be_true
|
16
|
+
@ability.can?(:wax, :fences).should be_false
|
17
|
+
@ability.can?(:paint, :cars).should be_false
|
18
|
+
end
|
19
|
+
|
20
|
+
it "allows access to everything when :access, :all is used" do
|
21
|
+
@ability.can?(:paint, :fences).should be_false
|
22
|
+
@ability.can :access, :all
|
23
|
+
@ability.can?(:paint, :fences).should be_true
|
24
|
+
@ability.can?(:wax, :fences).should be_true
|
25
|
+
@ability.can?(:paint, :cars).should be_true
|
26
|
+
end
|
27
|
+
|
28
|
+
it "allows access to multiple actions and subjects" do
|
29
|
+
@ability.can [:paint, :sand], [:fences, :decks]
|
30
|
+
@ability.can?(:paint, :fences).should be_true
|
31
|
+
@ability.can?(:sand, :fences).should be_true
|
32
|
+
@ability.can?(:paint, :decks).should be_true
|
33
|
+
@ability.can?(:sand, :decks).should be_true
|
34
|
+
@ability.can?(:wax, :fences).should be_false
|
35
|
+
@ability.can?(:paint, :cars).should be_false
|
36
|
+
end
|
37
|
+
|
38
|
+
it "allows strings instead of symbols in ability check" do
|
39
|
+
@ability.can :paint, :fences
|
40
|
+
@ability.can?("paint", "fences").should be_true
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# Aliases
|
45
|
+
|
46
|
+
it "has default index, show, new, update, delete aliases" do
|
47
|
+
@ability.can :read, :projects
|
48
|
+
@ability.can?(:index, :projects).should be_true
|
49
|
+
@ability.can?(:show, :projects).should be_true
|
50
|
+
@ability.can :create, :projects
|
51
|
+
@ability.can?(:new, :projects).should be_true
|
52
|
+
@ability.can :update, :projects
|
53
|
+
@ability.can?(:edit, :projects).should be_true
|
54
|
+
@ability.can :destroy, :projects
|
55
|
+
@ability.can?(:delete, :projects).should be_true
|
56
|
+
end
|
57
|
+
|
58
|
+
it "follows deep action aliases" do
|
59
|
+
@ability.alias_action :update, :destroy, :to => :modify
|
60
|
+
@ability.can :modify, :projects
|
61
|
+
@ability.can?(:update, :projects).should be_true
|
62
|
+
@ability.can?(:destroy, :projects).should be_true
|
63
|
+
@ability.can?(:edit, :projects).should be_true
|
64
|
+
end
|
65
|
+
|
66
|
+
it "adds up action aliases" do
|
67
|
+
@ability.alias_action :update, :to => :modify
|
68
|
+
@ability.alias_action :destroy, :to => :modify
|
69
|
+
@ability.can :modify, :projects
|
70
|
+
@ability.can?(:update, :projects).should be_true
|
71
|
+
@ability.can?(:destroy, :projects).should be_true
|
72
|
+
end
|
73
|
+
|
74
|
+
it "follows deep subject aliases" do
|
75
|
+
@ability.alias_subject :mammals, :to => :animals
|
76
|
+
@ability.alias_subject :cats, :to => :mammals
|
77
|
+
@ability.can :pet, :animals
|
78
|
+
@ability.can?(:pet, :mammals).should be_true
|
79
|
+
end
|
80
|
+
|
81
|
+
it "clears current and default aliases" do
|
82
|
+
@ability.alias_action :update, :destroy, :to => :modify
|
83
|
+
@ability.clear_aliases
|
84
|
+
@ability.can :modify, :projects
|
85
|
+
@ability.can?(:update, :projects).should be_false
|
86
|
+
@ability.can :read, :projects
|
87
|
+
@ability.can?(:show, :projects).should be_false
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
# Hash Conditions
|
92
|
+
|
93
|
+
it "maps object to pluralized subject name" do
|
94
|
+
@ability.can :read, :ranges
|
95
|
+
@ability.can?(:read, :ranges).should be_true
|
96
|
+
@ability.can?(:read, 1..3).should be_true
|
97
|
+
@ability.can?(:read, 123).should be_false
|
98
|
+
end
|
99
|
+
|
100
|
+
it "checks conditions hash on instances only" do
|
101
|
+
@ability.can :read, :ranges, :begin => 1
|
102
|
+
@ability.can?(:read, :ranges).should be_true
|
103
|
+
@ability.can?(:read, 1..3).should be_true
|
104
|
+
@ability.can?(:read, 2..4).should be_false
|
105
|
+
end
|
106
|
+
|
107
|
+
it "checks conditions on both rules and matches either one" do
|
108
|
+
@ability.can :read, :ranges, :begin => 1
|
109
|
+
@ability.can :read, :ranges, :begin => 2
|
110
|
+
@ability.can?(:read, 1..3).should be_true
|
111
|
+
@ability.can?(:read, 2..4).should be_true
|
112
|
+
@ability.can?(:read, 3..5).should be_false
|
113
|
+
end
|
114
|
+
|
115
|
+
it "checks an array of options in conditions hash" do
|
116
|
+
@ability.can :read, :ranges, :begin => [1, 3, 5]
|
117
|
+
@ability.can?(:read, 1..3).should be_true
|
118
|
+
@ability.can?(:read, 2..4).should be_false
|
119
|
+
@ability.can?(:read, 3..5).should be_true
|
120
|
+
end
|
121
|
+
|
122
|
+
it "checks a range of options in conditions hash" do
|
123
|
+
@ability.can :read, :ranges, :begin => 1..3
|
124
|
+
@ability.can?(:read, 1..10).should be_true
|
125
|
+
@ability.can?(:read, 3..30).should be_true
|
126
|
+
@ability.can?(:read, 4..40).should be_false
|
127
|
+
end
|
128
|
+
|
129
|
+
it "checks nested conditions hash" do
|
130
|
+
@ability.can :read, :ranges, :begin => { :to_i => 5 }
|
131
|
+
@ability.can?(:read, 5..7).should be_true
|
132
|
+
@ability.can?(:read, 6..8).should be_false
|
133
|
+
end
|
134
|
+
|
135
|
+
it "matches any element passed in to nesting if it's an array (for has_many associations)" do
|
136
|
+
@ability.can :read, :ranges, :to_a => { :to_i => 3 }
|
137
|
+
@ability.can?(:read, 1..5).should be_true
|
138
|
+
@ability.can?(:read, 4..6).should be_false
|
139
|
+
end
|
140
|
+
|
141
|
+
it "takes presedence over rule defined without a condition" do
|
142
|
+
@ability.can :read, :ranges
|
143
|
+
@ability.can :read, :ranges, :begin => 1
|
144
|
+
@ability.can?(:read, 1..5).should be_true
|
145
|
+
@ability.can?(:read, 4..6).should be_false
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
# Block Conditions
|
150
|
+
|
151
|
+
it "executes block passing object only when instance is used" do
|
152
|
+
@ability.can :read, :ranges do |range|
|
153
|
+
range.begin == 5
|
154
|
+
end
|
155
|
+
@ability.can?(:read, :ranges).should be_true
|
156
|
+
@ability.can?(:read, 5..7).should be_true
|
157
|
+
@ability.can?(:read, 6..8).should be_false
|
158
|
+
end
|
159
|
+
|
160
|
+
it "returns true when other object is returned in block" do
|
161
|
+
@ability.can :read, :ranges do |range|
|
162
|
+
"foo"
|
163
|
+
end
|
164
|
+
@ability.can?(:read, 5..7).should be_true
|
165
|
+
end
|
166
|
+
|
167
|
+
it "passes to previous rule when block returns false" do
|
168
|
+
@ability.can :read, :fixnums do |i|
|
169
|
+
i < 5
|
170
|
+
end
|
171
|
+
@ability.can :read, :fixnums do |i|
|
172
|
+
i > 10
|
173
|
+
end
|
174
|
+
@ability.can?(:read, 11).should be_true
|
175
|
+
@ability.can?(:read, 1).should be_true
|
176
|
+
@ability.can?(:read, 6).should be_false
|
177
|
+
end
|
178
|
+
|
179
|
+
it "calls block passing arguments when no arguments are given to can" do
|
180
|
+
@ability.can do |action, subject, object|
|
181
|
+
action.should == :read
|
182
|
+
subject.should == :ranges
|
183
|
+
object.should == (2..4)
|
184
|
+
@block_called = true
|
185
|
+
end
|
186
|
+
@ability.can?(:read, 2..4)
|
187
|
+
@block_called.should be_true
|
188
|
+
end
|
189
|
+
|
190
|
+
it "raises an error when attempting to use a block with a hash condition since it's not likely what they want" do
|
191
|
+
lambda {
|
192
|
+
@ability.can :read, :ranges, :published => true do
|
193
|
+
false
|
194
|
+
end
|
195
|
+
}.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.")
|
196
|
+
end
|
197
|
+
|
198
|
+
it "does not raise an error when attempting to use a block with an array of SQL conditions" do
|
199
|
+
lambda {
|
200
|
+
@ability.can :read, :ranges, ["published = ?", true] do
|
201
|
+
false
|
202
|
+
end
|
203
|
+
}.should_not raise_error(CanCan::Error)
|
204
|
+
end
|
205
|
+
|
206
|
+
|
207
|
+
# Attributes
|
208
|
+
|
209
|
+
it "allows permission on attributes" do
|
210
|
+
@ability.can :update, :users, :name
|
211
|
+
@ability.can :update, :users, [:email, :age]
|
212
|
+
@ability.can?(:update, :users, :name).should be_true
|
213
|
+
@ability.can?(:update, :users, :email).should be_true
|
214
|
+
@ability.can?(:update, :users, :password).should be_false
|
215
|
+
end
|
216
|
+
|
217
|
+
it "allows permission on all attributes when none are given" do
|
218
|
+
@ability.can :update, :users
|
219
|
+
@ability.can?(:update, :users, :password).should be_true
|
220
|
+
end
|
221
|
+
|
222
|
+
it "allows strings when chekcing attributes" do
|
223
|
+
@ability.can :update, :users, :name
|
224
|
+
@ability.can?(:update, :users, "name").should be_true
|
225
|
+
end
|
226
|
+
|
227
|
+
it "combines attribute check with conditions hash" do
|
228
|
+
@ability.can :update, :ranges, :begin => 1
|
229
|
+
@ability.can :update, :ranges, :name, :begin => 2
|
230
|
+
@ability.can?(:update, 1..3, :foobar).should be_true
|
231
|
+
@ability.can?(:update, 2..4, :foobar).should be_false
|
232
|
+
@ability.can?(:update, 2..4, :name).should be_true
|
233
|
+
@ability.can?(:update, 3..5, :name).should be_false
|
234
|
+
end
|
235
|
+
|
236
|
+
it "passes attribute to block and nil if no attribute checked" do
|
237
|
+
@ability.can :update, :ranges do |range, attribute|
|
238
|
+
attribute == :name
|
239
|
+
end
|
240
|
+
@ability.can?(:update, 1..3, :name).should be_true
|
241
|
+
@ability.can?(:update, 2..4).should be_false
|
242
|
+
end
|
243
|
+
|
244
|
+
it "passes attribute to block for global can definition" do
|
245
|
+
@ability.can do |action, subject, object, attribute|
|
246
|
+
attribute == :name
|
247
|
+
end
|
248
|
+
@ability.can?(:update, 1..3, :name).should be_true
|
249
|
+
@ability.can?(:update, 2..4).should be_false
|
250
|
+
end
|
251
|
+
|
252
|
+
|
253
|
+
# Checking if Fully Authorized
|
254
|
+
|
255
|
+
it "is not fully authorized when no authorize! call is made" do
|
256
|
+
@ability.can :update, :ranges, :begin => 1
|
257
|
+
@ability.can?(:update, :ranges).should be_true
|
258
|
+
@ability.should_not be_fully_authorized(:update, :ranges)
|
259
|
+
end
|
260
|
+
|
261
|
+
it "is fully authorized when calling authorize! with a matching action and subject" do
|
262
|
+
@ability.can :update, :ranges
|
263
|
+
@ability.authorize! :update, :ranges
|
264
|
+
@ability.should be_fully_authorized(:update, :ranges)
|
265
|
+
@ability.should_not be_fully_authorized(:create, :ranges)
|
266
|
+
end
|
267
|
+
|
268
|
+
it "is fully authorized when marking action and subject as such" do
|
269
|
+
@ability.fully_authorized! :update, :ranges
|
270
|
+
@ability.should be_fully_authorized(:update, :ranges)
|
271
|
+
end
|
272
|
+
|
273
|
+
it "is not fully authorized when a conditions hash exists but no instance is used" do
|
274
|
+
@ability.can :update, :ranges, :begin => 1
|
275
|
+
@ability.authorize! :update, :ranges
|
276
|
+
@ability.should_not be_fully_authorized(:update, :ranges)
|
277
|
+
@ability.authorize! "update", "ranges"
|
278
|
+
@ability.should_not be_fully_authorized(:update, :ranges)
|
279
|
+
@ability.authorize! :update, 1..3
|
280
|
+
@ability.should be_fully_authorized(:update, :ranges)
|
281
|
+
end
|
282
|
+
|
283
|
+
it "is not fully authorized when a block exists but no instance is used" do
|
284
|
+
@ability.can :update, :ranges do |range|
|
285
|
+
range.begin == 1
|
286
|
+
end
|
287
|
+
@ability.authorize! :update, :ranges
|
288
|
+
@ability.should_not be_fully_authorized(:update, :ranges)
|
289
|
+
@ability.authorize! :update, 1..3
|
290
|
+
@ability.should be_fully_authorized(:update, :ranges)
|
291
|
+
end
|
292
|
+
|
293
|
+
it "should accept a set as a condition value" do
|
294
|
+
object_with_foo_2 = Object.new
|
295
|
+
object_with_foo_2.should_receive(:foo) { 2 }
|
296
|
+
object_with_foo_3 = Object.new
|
297
|
+
object_with_foo_3.should_receive(:foo) { 3 }
|
298
|
+
@ability.can :read, :objects, :foo => [1, 2, 5].to_set
|
299
|
+
@ability.can?(:read, object_with_foo_2).should be_true
|
300
|
+
@ability.can?(:read, object_with_foo_3).should be_false
|
301
|
+
end
|
302
|
+
|
303
|
+
it "does not match subjects return nil for methods that must match nested a nested conditions hash" do
|
304
|
+
object_with_foo = Object.new
|
305
|
+
object_with_foo.should_receive(:foo) { :bar }
|
306
|
+
@ability.can :read, :arrays, :first => { :foo => :bar }
|
307
|
+
@ability.can?(:read, [object_with_foo]).should be_true
|
308
|
+
@ability.can?(:read, []).should be_false
|
309
|
+
end
|
310
|
+
|
311
|
+
it "is not fully authorized when attributes are required but not checked in update/create actions" do
|
312
|
+
@ability.can :access, :users, :name
|
313
|
+
@ability.authorize! :update, :users
|
314
|
+
@ability.should_not be_fully_authorized(:update, :users)
|
315
|
+
@ability.authorize! :create, :users
|
316
|
+
@ability.should_not be_fully_authorized(:create, :users)
|
317
|
+
@ability.authorize! :create, :users, :name
|
318
|
+
@ability.should be_fully_authorized(:create, :users)
|
319
|
+
@ability.authorize! :destroy, :users
|
320
|
+
@ability.should be_fully_authorized(:destroy, :users)
|
321
|
+
end
|
322
|
+
|
323
|
+
it "marks as fully authorized when authorizing with strings instead of symbols" do
|
324
|
+
@ability.fully_authorized! "update", "ranges"
|
325
|
+
@ability.should be_fully_authorized(:update, :ranges)
|
326
|
+
@ability.should be_fully_authorized("update", "ranges")
|
327
|
+
@ability.can :update, :users
|
328
|
+
@ability.authorize! "update", "users"
|
329
|
+
@ability.should be_fully_authorized(:update, :users)
|
330
|
+
end
|
331
|
+
|
332
|
+
|
333
|
+
# Cannot
|
334
|
+
|
335
|
+
it "offers cannot? method which inverts can?" do
|
336
|
+
@ability.cannot?(:wax, :cars).should be_true
|
337
|
+
end
|
338
|
+
|
339
|
+
it "supports 'cannot' method to define what user cannot do" do
|
340
|
+
@ability.can :read, :all
|
341
|
+
@ability.cannot :read, :ranges
|
342
|
+
@ability.can?(:read, :books).should be_true
|
343
|
+
@ability.can?(:read, 1..3).should be_false
|
344
|
+
@ability.can?(:read, :ranges).should be_false
|
345
|
+
end
|
346
|
+
|
347
|
+
it "passes to previous rule if cannot check returns false" do
|
348
|
+
@ability.can :read, :all
|
349
|
+
@ability.cannot :read, :ranges, :begin => 3
|
350
|
+
@ability.cannot :read, :ranges do |range|
|
351
|
+
range.begin == 5
|
352
|
+
end
|
353
|
+
@ability.can?(:read, :books).should be_true
|
354
|
+
@ability.can?(:read, 2..4).should be_true
|
355
|
+
@ability.can?(:read, 3..7).should be_false
|
356
|
+
@ability.can?(:read, 5..9).should be_false
|
357
|
+
end
|
358
|
+
|
359
|
+
it "rejects permission only to a given attribute" do
|
360
|
+
@ability.can :update, :books
|
361
|
+
@ability.cannot :update, :books, :author
|
362
|
+
@ability.can?(:update, :books).should be_true
|
363
|
+
@ability.can?(:update, :books, :author).should be_false
|
364
|
+
end
|
365
|
+
|
366
|
+
# Hash Association
|
367
|
+
|
368
|
+
it "checks permission through association when hash is passed as subject" do
|
369
|
+
@ability.can :read, :books, :range => {:begin => 3}
|
370
|
+
@ability.can?(:read, (1..4) => :books).should be_false
|
371
|
+
@ability.can?(:read, (3..5) => :books).should be_true
|
372
|
+
@ability.can?(:read, 123 => :books).should be_true
|
373
|
+
end
|
374
|
+
|
375
|
+
it "checks permissions on association hash with multiple rules" do
|
376
|
+
@ability.can :read, :books, :range => {:begin => 3}
|
377
|
+
@ability.can :read, :books, :range => {:end => 6}
|
378
|
+
@ability.can?(:read, (1..4) => :books).should be_false
|
379
|
+
@ability.can?(:read, (3..5) => :books).should be_true
|
380
|
+
@ability.can?(:read, (1..6) => :books).should be_true
|
381
|
+
@ability.can?(:read, 123 => :books).should be_true
|
382
|
+
end
|
383
|
+
|
384
|
+
it "checks ability on hash subclass" do
|
385
|
+
class Container < Hash; end
|
386
|
+
@ability.can :read, :containers
|
387
|
+
@ability.can?(:read, Container.new).should be_true
|
388
|
+
end
|
389
|
+
|
390
|
+
|
391
|
+
# Initial Attributes
|
392
|
+
|
393
|
+
it "has initial attributes based on hash conditions for a given action" do
|
394
|
+
@ability.can :access, :ranges, :foo => "foo", :hash => {:skip => "hashes"}
|
395
|
+
@ability.can :create, :ranges, :bar => 123, :array => %w[skip arrays]
|
396
|
+
@ability.can :new, :ranges, :baz => "baz", :range => 1..3
|
397
|
+
@ability.cannot :new, :ranges, :ignore => "me"
|
398
|
+
@ability.attributes_for(:new, :ranges).should == {:foo => "foo", :bar => 123, :baz => "baz"}
|
399
|
+
end
|
400
|
+
|
401
|
+
|
402
|
+
# Unauthorized Exception
|
403
|
+
|
404
|
+
it "raises CanCan::Unauthorized when calling authorize! on unauthorized action" do
|
405
|
+
begin
|
406
|
+
@ability.authorize! :read, :books, :message => "Access denied!"
|
407
|
+
rescue CanCan::Unauthorized => e
|
408
|
+
e.message.should == "Access denied!"
|
409
|
+
e.action.should == :read
|
410
|
+
e.subject.should == :books
|
411
|
+
else
|
412
|
+
fail "Expected CanCan::Unauthorized exception to be raised"
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
it "does not raise access denied exception if ability is authorized to perform an action and return subject" do
|
417
|
+
@ability.can :read, :foo
|
418
|
+
lambda {
|
419
|
+
@ability.authorize!(:read, :foo).should == :foo
|
420
|
+
}.should_not raise_error
|
421
|
+
end
|
422
|
+
|
423
|
+
it "knows when block is used in conditions" do
|
424
|
+
@ability.can :read, :foo
|
425
|
+
@ability.should_not have_block(:read, :foo)
|
426
|
+
@ability.can :read, :foo do |foo|
|
427
|
+
false
|
428
|
+
end
|
429
|
+
@ability.should have_block(:read, :foo)
|
430
|
+
end
|
431
|
+
|
432
|
+
it "knows when raw sql is used in conditions" do
|
433
|
+
@ability.can :read, :foo
|
434
|
+
@ability.should_not have_raw_sql(:read, :foo)
|
435
|
+
@ability.can :read, :foo, 'false'
|
436
|
+
@ability.should have_raw_sql(:read, :foo)
|
437
|
+
end
|
438
|
+
|
439
|
+
it "raises access denied exception with default message if not specified" do
|
440
|
+
begin
|
441
|
+
@ability.authorize! :read, :books
|
442
|
+
rescue CanCan::Unauthorized => e
|
443
|
+
e.default_message = "Access denied!"
|
444
|
+
e.message.should == "Access denied!"
|
445
|
+
else
|
446
|
+
fail "Expected CanCan::Unauthorized exception to be raised"
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
it "does not raise access denied exception if ability is authorized to perform an action and return subject" do
|
451
|
+
@ability.can :read, :books
|
452
|
+
lambda {
|
453
|
+
@ability.authorize!(:read, :books).should == :books
|
454
|
+
}.should_not raise_error
|
455
|
+
end
|
456
|
+
|
457
|
+
|
458
|
+
# Determining Kind of Conditions
|
459
|
+
|
460
|
+
it "knows when a block is used for conditions" do
|
461
|
+
@ability.can :read, :books
|
462
|
+
@ability.should_not have_block(:read, :books)
|
463
|
+
@ability.can :read, :books do |foo|
|
464
|
+
false
|
465
|
+
end
|
466
|
+
@ability.should have_block(:read, :books)
|
467
|
+
end
|
468
|
+
|
469
|
+
it "knows when raw sql is used for conditions" do
|
470
|
+
@ability.can :read, :books
|
471
|
+
@ability.should_not have_raw_sql(:read, :books)
|
472
|
+
@ability.can :read, :books, 'false'
|
473
|
+
@ability.should have_raw_sql(:read, :books)
|
474
|
+
end
|
475
|
+
|
476
|
+
it "determines model adapter class by asking AbstractAdapter" do
|
477
|
+
model_class = Object.new
|
478
|
+
adapter_class = Object.new
|
479
|
+
CanCan::ModelAdapters::AbstractAdapter.stub(:adapter_class).with(model_class) { adapter_class }
|
480
|
+
adapter_class.stub(:new).with(model_class, []) { :adapter_instance }
|
481
|
+
@ability.model_adapter(model_class, :read).should == :adapter_instance
|
482
|
+
end
|
483
|
+
|
484
|
+
|
485
|
+
# Unauthorized I18n Message
|
486
|
+
|
487
|
+
describe "unauthorized message" do
|
488
|
+
after(:each) do
|
489
|
+
I18n.backend = nil
|
490
|
+
end
|
491
|
+
|
492
|
+
it "uses action/subject in i18n" do
|
493
|
+
I18n.backend.store_translations :en, :unauthorized => {:update => {:ranges => "update ranges"}}
|
494
|
+
@ability.unauthorized_message(:update, :ranges).should == "update ranges"
|
495
|
+
@ability.unauthorized_message(:update, 2..4).should == "update ranges"
|
496
|
+
@ability.unauthorized_message(:update, :missing).should be_nil
|
497
|
+
end
|
498
|
+
|
499
|
+
it "uses symbol as subject directly" do
|
500
|
+
I18n.backend.store_translations :en, :unauthorized => {:has => {:cheezburger => "Nom nom nom. I eated it."}}
|
501
|
+
@ability.unauthorized_message(:has, :cheezburger).should == "Nom nom nom. I eated it."
|
502
|
+
end
|
503
|
+
|
504
|
+
it "falls back to 'access' and 'all'" do
|
505
|
+
I18n.backend.store_translations :en, :unauthorized => {
|
506
|
+
:access => {:all => "access all", :ranges => "access ranges"},
|
507
|
+
:update => {:all => "update all", :ranges => "update ranges"}
|
508
|
+
}
|
509
|
+
@ability.unauthorized_message(:update, :ranges).should == "update ranges"
|
510
|
+
@ability.unauthorized_message(:update, :hashes).should == "update all"
|
511
|
+
@ability.unauthorized_message(:create, :ranges).should == "access ranges"
|
512
|
+
@ability.unauthorized_message(:create, :hashes).should == "access all"
|
513
|
+
end
|
514
|
+
|
515
|
+
it "follows aliases" do
|
516
|
+
I18n.backend.store_translations :en, :unauthorized => {:modify => {:ranges => "modify ranges"}}
|
517
|
+
@ability.alias_action :update, :to => :modify
|
518
|
+
@ability.alias_subject :areas, :to => :ranges
|
519
|
+
@ability.unauthorized_message(:update, :areas).should == "modify ranges"
|
520
|
+
@ability.unauthorized_message(:edit, :ranges).should == "modify ranges"
|
521
|
+
end
|
522
|
+
|
523
|
+
it "has variables for action and subject" do
|
524
|
+
I18n.backend.store_translations :en, :unauthorized => {:access => {:all => "%{action} %{subject}"}} # old syntax for now in case testing with old I18n
|
525
|
+
@ability.unauthorized_message(:update, :ranges).should == "update ranges"
|
526
|
+
@ability.unauthorized_message(:edit, 1..3).should == "edit ranges"
|
527
|
+
# @ability.unauthorized_message(:update, ArgumentError).should == "update argument error"
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
it "merges the rules from another ability" do
|
532
|
+
@ability.can :use, :tools
|
533
|
+
another_ability = Object.new
|
534
|
+
another_ability.extend(CanCan::Ability)
|
535
|
+
another_ability.can :use, :search
|
536
|
+
|
537
|
+
@ability.merge(another_ability)
|
538
|
+
@ability.can?(:use, :search).should be_true
|
539
|
+
@ability.send(:rules).size.should == 2
|
540
|
+
end
|
541
|
+
end
|