cancancan 1.7.0

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.
Files changed (42) hide show
  1. checksums.yaml +15 -0
  2. data/CHANGELOG.rdoc +427 -0
  3. data/CONTRIBUTING.md +11 -0
  4. data/Gemfile +23 -0
  5. data/LICENSE +20 -0
  6. data/README.rdoc +161 -0
  7. data/Rakefile +18 -0
  8. data/init.rb +1 -0
  9. data/lib/cancan.rb +13 -0
  10. data/lib/cancan/ability.rb +324 -0
  11. data/lib/cancan/controller_additions.rb +397 -0
  12. data/lib/cancan/controller_resource.rb +286 -0
  13. data/lib/cancan/exceptions.rb +50 -0
  14. data/lib/cancan/inherited_resource.rb +20 -0
  15. data/lib/cancan/matchers.rb +14 -0
  16. data/lib/cancan/model_adapters/abstract_adapter.rb +56 -0
  17. data/lib/cancan/model_adapters/active_record_adapter.rb +180 -0
  18. data/lib/cancan/model_adapters/data_mapper_adapter.rb +34 -0
  19. data/lib/cancan/model_adapters/default_adapter.rb +7 -0
  20. data/lib/cancan/model_adapters/mongoid_adapter.rb +54 -0
  21. data/lib/cancan/model_additions.rb +31 -0
  22. data/lib/cancan/rule.rb +147 -0
  23. data/lib/cancancan.rb +1 -0
  24. data/lib/generators/cancan/ability/USAGE +4 -0
  25. data/lib/generators/cancan/ability/ability_generator.rb +11 -0
  26. data/lib/generators/cancan/ability/templates/ability.rb +32 -0
  27. data/spec/README.rdoc +28 -0
  28. data/spec/cancan/ability_spec.rb +455 -0
  29. data/spec/cancan/controller_additions_spec.rb +141 -0
  30. data/spec/cancan/controller_resource_spec.rb +553 -0
  31. data/spec/cancan/exceptions_spec.rb +58 -0
  32. data/spec/cancan/inherited_resource_spec.rb +60 -0
  33. data/spec/cancan/matchers_spec.rb +29 -0
  34. data/spec/cancan/model_adapters/active_record_adapter_spec.rb +358 -0
  35. data/spec/cancan/model_adapters/data_mapper_adapter_spec.rb +118 -0
  36. data/spec/cancan/model_adapters/default_adapter_spec.rb +7 -0
  37. data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +226 -0
  38. data/spec/cancan/rule_spec.rb +52 -0
  39. data/spec/matchers.rb +13 -0
  40. data/spec/spec.opts +2 -0
  41. data/spec/spec_helper.rb +77 -0
  42. metadata +126 -0
@@ -0,0 +1,147 @@
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
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, subject, conditions, block)
14
+ raise Error, "You are not able to supply a block with a hash of conditions in #{action} #{subject} ability. Use either one." if conditions.kind_of?(Hash) && !block.nil?
15
+ @match_all = action.nil? && subject.nil?
16
+ @base_behavior = base_behavior
17
+ @actions = [action].flatten
18
+ @subjects = [subject].flatten
19
+ @conditions = conditions || {}
20
+ @block = block
21
+ end
22
+
23
+ # Matches both the subject and action, not necessarily the conditions
24
+ def relevant?(action, subject)
25
+ subject = subject.values.first if subject.class == Hash
26
+ @match_all || (matches_action?(action) && matches_subject?(subject))
27
+ end
28
+
29
+ # Matches the block or conditions hash
30
+ def matches_conditions?(action, subject, extra_args)
31
+ if @match_all
32
+ call_block_with_all(action, subject, extra_args)
33
+ elsif @block && !subject_class?(subject)
34
+ @block.call(subject, *extra_args)
35
+ elsif @conditions.kind_of?(Hash) && subject.class == Hash
36
+ nested_subject_matches_conditions?(subject)
37
+ elsif @conditions.kind_of?(Hash) && !subject_class?(subject)
38
+ matches_conditions_hash?(subject)
39
+ else
40
+ # Don't stop at "cannot" definitions when there are conditions.
41
+ conditions_empty? ? true : @base_behavior
42
+ end
43
+ end
44
+
45
+ def only_block?
46
+ conditions_empty? && !@block.nil?
47
+ end
48
+
49
+ def only_raw_sql?
50
+ @block.nil? && !conditions_empty? && !@conditions.kind_of?(Hash)
51
+ end
52
+
53
+ def conditions_empty?
54
+ @conditions == {} || @conditions.nil?
55
+ end
56
+
57
+ def unmergeable?
58
+ @conditions.respond_to?(:keys) && @conditions.present? &&
59
+ (!@conditions.keys.first.kind_of? Symbol)
60
+ end
61
+
62
+ def associations_hash(conditions = @conditions)
63
+ hash = {}
64
+ conditions.map do |name, value|
65
+ hash[name] = associations_hash(value) if value.kind_of? Hash
66
+ end if conditions.kind_of? Hash
67
+ hash
68
+ end
69
+
70
+ def attributes_from_conditions
71
+ attributes = {}
72
+ @conditions.each do |key, value|
73
+ attributes[key] = value unless [Array, Range, Hash].include? value.class
74
+ end if @conditions.kind_of? Hash
75
+ attributes
76
+ end
77
+
78
+ private
79
+
80
+ def subject_class?(subject)
81
+ klass = (subject.kind_of?(Hash) ? subject.values.first : subject).class
82
+ klass == Class || klass == Module
83
+ end
84
+
85
+ def matches_action?(action)
86
+ @expanded_actions.include?(:manage) || @expanded_actions.include?(action)
87
+ end
88
+
89
+ def matches_subject?(subject)
90
+ @subjects.include?(:all) || @subjects.include?(subject) || matches_subject_class?(subject)
91
+ end
92
+
93
+ def matches_subject_class?(subject)
94
+ @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)) }
95
+ end
96
+
97
+ # Checks if the given subject matches the given conditions hash.
98
+ # This behavior can be overriden by a model adapter by defining two class methods:
99
+ # override_matching_for_conditions?(subject, conditions) and
100
+ # matches_conditions_hash?(subject, conditions)
101
+ def matches_conditions_hash?(subject, conditions = @conditions)
102
+ if conditions.empty?
103
+ true
104
+ else
105
+ if model_adapter(subject).override_conditions_hash_matching? subject, conditions
106
+ model_adapter(subject).matches_conditions_hash? subject, conditions
107
+ else
108
+ conditions.all? do |name, value|
109
+ if model_adapter(subject).override_condition_matching? subject, name, value
110
+ model_adapter(subject).matches_condition? subject, name, value
111
+ else
112
+ attribute = subject.send(name)
113
+ if value.kind_of?(Hash)
114
+ if attribute.kind_of?(Array) || attribute.kind_of?(ActiveRecord::Relation)
115
+ attribute.any? { |element| matches_conditions_hash? element, value }
116
+ else
117
+ !attribute.nil? && matches_conditions_hash?(attribute, value)
118
+ end
119
+ elsif !value.is_a?(String) && value.kind_of?(Enumerable)
120
+ value.include? attribute
121
+ else
122
+ attribute == value
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ def nested_subject_matches_conditions?(subject_hash)
131
+ parent, child = subject_hash.first
132
+ matches_conditions_hash?(parent, @conditions[parent.class.name.downcase.to_sym] || {})
133
+ end
134
+
135
+ def call_block_with_all(action, subject, extra_args)
136
+ if subject.class == Class
137
+ @block.call(action, subject, nil, *extra_args)
138
+ else
139
+ @block.call(action, subject.class, subject, *extra_args)
140
+ end
141
+ end
142
+
143
+ def model_adapter(subject)
144
+ CanCan::ModelAdapters::AbstractAdapter.adapter_class(subject_class?(subject) ? subject : subject.class)
145
+ end
146
+ end
147
+ end
@@ -0,0 +1 @@
1
+ require 'cancan'
@@ -0,0 +1,4 @@
1
+ Description:
2
+ The cancan:ability generator creates an Ability class in the models
3
+ directory. You can move this file anywhere you want as long as it
4
+ is in the load path.
@@ -0,0 +1,11 @@
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
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,32 @@
1
+ class Ability
2
+ include CanCan::Ability
3
+
4
+ def initialize(user)
5
+ # Define abilities for the passed in user here. For example:
6
+ #
7
+ # user ||= User.new # guest user (not logged in)
8
+ # if user.admin?
9
+ # can :manage, :all
10
+ # else
11
+ # can :read, :all
12
+ # end
13
+ #
14
+ # The first argument to `can` is the action you are giving the user
15
+ # permission to do.
16
+ # If you pass :manage it will apply to every action. Other common actions
17
+ # here are :read, :create, :update and :destroy.
18
+ #
19
+ # The second argument is the resource the user can perform the action on.
20
+ # If you pass :all it will apply to every resource. Otherwise pass a Ruby
21
+ # class of the resource.
22
+ #
23
+ # The third argument is an optional hash of conditions to further filter the
24
+ # objects.
25
+ # For example, here the user can only update published articles.
26
+ #
27
+ # can :update, Article, :published => true
28
+ #
29
+ # See the wiki for details:
30
+ # https://github.com/bryanrite/cancancan/wiki/Defining-Abilities
31
+ end
32
+ end
@@ -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,455 @@
1
+ require "spec_helper"
2
+
3
+ describe CanCan::Ability do
4
+ before(:each) do
5
+ (@ability = double).extend(CanCan::Ability)
6
+ end
7
+
8
+ it "is able to :read anything" do
9
+ @ability.can :read, :all
10
+ expect(@ability.can?(:read, String)).to be_true
11
+ expect(@ability.can?(:read, 123)).to be_true
12
+ end
13
+
14
+ it "does not have permission to do something it doesn't know about" do
15
+ expect(@ability.can?(:foodfight, String)).to be_false
16
+ end
17
+
18
+ it "passes true to `can?` when non false/nil is returned in block" do
19
+ @ability.can :read, :all
20
+ @ability.can :read, Symbol do |sym|
21
+ "foo" # TODO test that sym is nil when no instance is passed
22
+ end
23
+ expect(@ability.can?(:read, :some_symbol)).to be_true
24
+ end
25
+
26
+ it "passes nil to a block when no instance is passed" do
27
+ @ability.can :read, Symbol do |sym|
28
+ expect(sym).to be_nil
29
+ true
30
+ end
31
+ expect(@ability.can?(:read, Symbol)).to be_true
32
+ end
33
+
34
+ it "passes to previous rule, if block returns false or nil" do
35
+ @ability.can :read, Symbol
36
+ @ability.can :read, Integer do |i|
37
+ i < 5
38
+ end
39
+ @ability.can :read, Integer do |i|
40
+ i > 10
41
+ end
42
+ expect(@ability.can?(:read, Symbol)).to be_true
43
+ expect(@ability.can?(:read, 11)).to be_true
44
+ expect(@ability.can?(:read, 1)).to be_true
45
+ expect(@ability.can?(:read, 6)).to be_false
46
+ end
47
+
48
+ it "does not pass class with object if :all objects are accepted" do
49
+ @ability.can :preview, :all do |object|
50
+ expect(object).to eq(123)
51
+ @block_called = true
52
+ end
53
+ @ability.can?(:preview, 123)
54
+ expect(@block_called).to be_true
55
+ end
56
+
57
+ it "does not call block when only class is passed, only return true" do
58
+ @block_called = false
59
+ @ability.can :preview, :all do |object|
60
+ @block_called = true
61
+ end
62
+ expect(@ability.can?(:preview, Hash)).to be_true
63
+ expect(@block_called).to be_false
64
+ end
65
+
66
+ it "passes only object for global manage actions" do
67
+ @ability.can :manage, String do |object|
68
+ expect(object).to eq("foo")
69
+ @block_called = true
70
+ end
71
+ expect(@ability.can?(:stuff, "foo")).to be_true
72
+ expect(@block_called).to be_true
73
+ end
74
+
75
+ it "makes alias for update or destroy actions to modify action" do
76
+ @ability.alias_action :update, :destroy, :to => :modify
77
+ @ability.can :modify, :all
78
+ expect(@ability.can?(:update, 123)).to be_true
79
+ expect(@ability.can?(:destroy, 123)).to be_true
80
+ end
81
+
82
+ it "allows deeply nested aliased actions" do
83
+ @ability.alias_action :increment, :to => :sort
84
+ @ability.alias_action :sort, :to => :modify
85
+ @ability.can :modify, :all
86
+ expect(@ability.can?(:increment, 123)).to be_true
87
+ end
88
+
89
+ it "raises an Error if alias target is an exist action" do
90
+ expect { @ability.alias_action :show, :to => :show }.to raise_error(CanCan::Error, "You can't specify target (show) as alias because it is real action name")
91
+ end
92
+
93
+ it "always calls block with arguments when passing no arguments to can" do
94
+ @ability.can do |action, object_class, object|
95
+ expect(action).to eq(:foo)
96
+ expect(object_class).to eq(123.class)
97
+ expect(object).to eq(123)
98
+ @block_called = true
99
+ end
100
+ @ability.can?(:foo, 123)
101
+ expect(@block_called).to be_true
102
+ end
103
+
104
+ it "passes nil to object when comparing class with can check" do
105
+ @ability.can do |action, object_class, object|
106
+ expect(action).to eq(:foo)
107
+ expect(object_class).to eq(Hash)
108
+ expect(object).to be_nil
109
+ @block_called = true
110
+ end
111
+ @ability.can?(:foo, Hash)
112
+ expect(@block_called).to be_true
113
+ end
114
+
115
+ it "automatically makes alias for index and show into read calls" do
116
+ @ability.can :read, :all
117
+ expect(@ability.can?(:index, 123)).to be_true
118
+ expect(@ability.can?(:show, 123)).to be_true
119
+ end
120
+
121
+ it "automatically makes alias for new and edit into create and update respectively" do
122
+ @ability.can :create, :all
123
+ @ability.can :update, :all
124
+ expect(@ability.can?(:new, 123)).to be_true
125
+ expect(@ability.can?(:edit, 123)).to be_true
126
+ end
127
+
128
+ it "does not respond to prepare (now using initialize)" do
129
+ expect(@ability).to_not respond_to(:prepare)
130
+ end
131
+
132
+ it "offers cannot? method which is simply invert of can?" do
133
+ expect(@ability.cannot?(:tie, String)).to be_true
134
+ end
135
+
136
+ it "is able to specify multiple actions and match any" do
137
+ @ability.can [:read, :update], :all
138
+ expect(@ability.can?(:read, 123)).to be_true
139
+ expect(@ability.can?(:update, 123)).to be_true
140
+ expect(@ability.can?(:count, 123)).to be_false
141
+ end
142
+
143
+ it "is able to specify multiple classes and match any" do
144
+ @ability.can :update, [String, Range]
145
+ expect(@ability.can?(:update, "foo")).to be_true
146
+ expect(@ability.can?(:update, 1..3)).to be_true
147
+ expect(@ability.can?(:update, 123)).to be_false
148
+ end
149
+
150
+ it "supports custom objects in the rule" do
151
+ @ability.can :read, :stats
152
+ expect(@ability.can?(:read, :stats)).to be_true
153
+ expect(@ability.can?(:update, :stats)).to be_false
154
+ expect(@ability.can?(:read, :nonstats)).to be_false
155
+ end
156
+
157
+ it "checks ancestors of class" do
158
+ @ability.can :read, Numeric
159
+ expect(@ability.can?(:read, Integer)).to be_true
160
+ expect(@ability.can?(:read, 1.23)).to be_true
161
+ expect(@ability.can?(:read, "foo")).to be_false
162
+ end
163
+
164
+ it "supports 'cannot' method to define what user cannot do" do
165
+ @ability.can :read, :all
166
+ @ability.cannot :read, Integer
167
+ expect(@ability.can?(:read, "foo")).to be_true
168
+ expect(@ability.can?(:read, 123)).to be_false
169
+ end
170
+
171
+ it "passes to previous rule, if block returns false or nil" do
172
+ @ability.can :read, :all
173
+ @ability.cannot :read, Integer do |int|
174
+ int > 10 ? nil : ( int > 5 )
175
+ end
176
+ expect(@ability.can?(:read, "foo")).to be_true
177
+ expect(@ability.can?(:read, 3)).to be_true
178
+ expect(@ability.can?(:read, 8)).to be_false
179
+ expect(@ability.can?(:read, 123)).to be_true
180
+ end
181
+
182
+ it "always returns `false` for single cannot definition" do
183
+ @ability.cannot :read, Integer do |int|
184
+ int > 10 ? nil : ( int > 5 )
185
+ end
186
+ expect(@ability.can?(:read, "foo")).to be_false
187
+ expect(@ability.can?(:read, 3)).to be_false
188
+ expect(@ability.can?(:read, 8)).to be_false
189
+ expect(@ability.can?(:read, 123)).to be_false
190
+ end
191
+
192
+ it "passes to previous cannot definition, if block returns false or nil" do
193
+ @ability.cannot :read, :all
194
+ @ability.can :read, Integer do |int|
195
+ int > 10 ? nil : ( int > 5 )
196
+ end
197
+ expect(@ability.can?(:read, "foo")).to be_false
198
+ expect(@ability.can?(:read, 3)).to be_false
199
+ expect(@ability.can?(:read, 10)).to be_true
200
+ expect(@ability.can?(:read, 123)).to be_false
201
+ end
202
+
203
+ it "appends aliased actions" do
204
+ @ability.alias_action :update, :to => :modify
205
+ @ability.alias_action :destroy, :to => :modify
206
+ expect(@ability.aliased_actions[:modify]).to eq([:update, :destroy])
207
+ end
208
+
209
+ it "clears aliased actions" do
210
+ @ability.alias_action :update, :to => :modify
211
+ @ability.clear_aliased_actions
212
+ expect(@ability.aliased_actions[:modify]).to be_nil
213
+ end
214
+
215
+ it "passes additional arguments to block from can?" do
216
+ @ability.can :read, Integer do |int, x|
217
+ int > x
218
+ end
219
+ expect(@ability.can?(:read, 2, 1)).to be_true
220
+ expect(@ability.can?(:read, 2, 3)).to be_false
221
+ end
222
+
223
+ it "uses conditions as third parameter and determine abilities from it" do
224
+ @ability.can :read, Range, :begin => 1, :end => 3
225
+ expect(@ability.can?(:read, 1..3)).to be_true
226
+ expect(@ability.can?(:read, 1..4)).to be_false
227
+ expect(@ability.can?(:read, Range)).to be_true
228
+ end
229
+
230
+ it "allows an array of options in conditions hash" do
231
+ @ability.can :read, Range, :begin => [1, 3, 5]
232
+ expect(@ability.can?(:read, 1..3)).to be_true
233
+ expect(@ability.can?(:read, 2..4)).to be_false
234
+ expect(@ability.can?(:read, 3..5)).to be_true
235
+ end
236
+
237
+ it "allows a range of options in conditions hash" do
238
+ @ability.can :read, Range, :begin => 1..3
239
+ expect(@ability.can?(:read, 1..10)).to be_true
240
+ expect(@ability.can?(:read, 3..30)).to be_true
241
+ expect(@ability.can?(:read, 4..40)).to be_false
242
+ end
243
+
244
+ it "allows nested hashes in conditions hash" do
245
+ @ability.can :read, Range, :begin => { :to_i => 5 }
246
+ expect(@ability.can?(:read, 5..7)).to be_true
247
+ expect(@ability.can?(:read, 6..8)).to be_false
248
+ end
249
+
250
+ it "matches any element passed in to nesting if it's an array (for has_many associations)" do
251
+ @ability.can :read, Range, :to_a => { :to_i => 3 }
252
+ expect(@ability.can?(:read, 1..5)).to be_true
253
+ expect(@ability.can?(:read, 4..6)).to be_false
254
+ end
255
+
256
+ it "accepts a set as a condition value" do
257
+ expect(object_with_foo_2 = double(:foo => 2)).to receive(:foo)
258
+ expect(object_with_foo_3 = double(:foo => 3)).to receive(:foo)
259
+ @ability.can :read, Object, :foo => [1, 2, 5].to_set
260
+ expect(@ability.can?(:read, object_with_foo_2)).to be_true
261
+ expect(@ability.can?(:read, object_with_foo_3)).to be_false
262
+ end
263
+
264
+ it "does not match subjects return nil for methods that must match nested a nested conditions hash" do
265
+ expect(object_with_foo = double(:foo => :bar)).to receive(:foo)
266
+ @ability.can :read, Array, :first => { :foo => :bar }
267
+ expect(@ability.can?(:read, [object_with_foo])).to be_true
268
+ expect(@ability.can?(:read, [])).to be_false
269
+ end
270
+
271
+ it "matches strings but not substrings specified in a conditions hash" do
272
+ @ability.can :read, String, :presence => "declassified"
273
+ expect(@ability.can?(:read, "declassified")).to be_true
274
+ expect(@ability.can?(:read, "classified")).to be_false
275
+ end
276
+
277
+ it "does not stop at cannot definition when comparing class" do
278
+ @ability.can :read, Range
279
+ @ability.cannot :read, Range, :begin => 1
280
+ expect(@ability.can?(:read, 2..5)).to be_true
281
+ expect(@ability.can?(:read, 1..5)).to be_false
282
+ expect(@ability.can?(:read, Range)).to be_true
283
+ end
284
+
285
+ it "stops at cannot definition when no hash is present" do
286
+ @ability.can :read, :all
287
+ @ability.cannot :read, Range
288
+ expect(@ability.can?(:read, 1..5)).to be_false
289
+ expect(@ability.can?(:read, Range)).to be_false
290
+ end
291
+
292
+ it "allows to check ability for Module" do
293
+ module B; end
294
+ class A; include B; end
295
+ @ability.can :read, B
296
+ expect(@ability.can?(:read, A)).to be_true
297
+ expect(@ability.can?(:read, A.new)).to be_true
298
+ end
299
+
300
+ it "passes nil to a block for ability on Module when no instance is passed" do
301
+ module B; end
302
+ class A; include B; end
303
+ @ability.can :read, B do |sym|
304
+ expect(sym).to be_nil
305
+ true
306
+ end
307
+ expect(@ability.can?(:read, B)).to be_true
308
+ expect(@ability.can?(:read, A)).to be_true
309
+ end
310
+
311
+ it "checks permissions through association when passing a hash of subjects" do
312
+ @ability.can :read, Range, :string => {:length => 3}
313
+ expect(@ability.can?(:read, "foo" => Range)).to be_true
314
+ expect(@ability.can?(:read, "foobar" => Range)).to be_false
315
+ expect(@ability.can?(:read, 123 => Range)).to be_true
316
+ end
317
+
318
+ it "checks permissions correctly when passing a hash of subjects with multiple definitions" do
319
+ @ability.can :read, Range, :string => {:length => 4}
320
+ @ability.can [:create, :read], Range, :string => {:upcase => 'FOO'}
321
+ expect(@ability.can?(:read, "foo" => Range)).to be_true
322
+ expect(@ability.can?(:read, "foobar" => Range)).to be_false
323
+ expect(@ability.can?(:read, 1234 => Range)).to be_true
324
+ end
325
+
326
+ it "allows to check ability on Hash-like object" do
327
+ class Container < Hash; end
328
+ @ability.can :read, Container
329
+ expect(@ability.can?(:read, Container.new)).to be_true
330
+ end
331
+
332
+ it "has initial attributes based on hash conditions of 'new' action" do
333
+ @ability.can :manage, Range, :foo => "foo", :hash => {:skip => "hashes"}
334
+ @ability.can :create, Range, :bar => 123, :array => %w[skip arrays]
335
+ @ability.can :new, Range, :baz => "baz", :range => 1..3
336
+ @ability.cannot :new, Range, :ignore => "me"
337
+ expect(@ability.attributes_for(:new, Range)).to eq({:foo => "foo", :bar => 123, :baz => "baz"})
338
+ end
339
+
340
+ it "raises access denied exception if ability us unauthorized to perform a certain action" do
341
+ begin
342
+ @ability.authorize! :read, :foo, 1, 2, 3, :message => "Access denied!"
343
+ rescue CanCan::AccessDenied => e
344
+ expect(e.message).to eq("Access denied!")
345
+ expect(e.action).to eq(:read)
346
+ expect(e.subject).to eq(:foo)
347
+ else
348
+ fail "Expected CanCan::AccessDenied exception to be raised"
349
+ end
350
+ end
351
+
352
+ it "does not raise access denied exception if ability is authorized to perform an action and return subject" do
353
+ @ability.can :read, :foo
354
+ expect {
355
+ expect(@ability.authorize!(:read, :foo)).to eq(:foo)
356
+ }.to_not raise_error
357
+ end
358
+
359
+ it "knows when block is used in conditions" do
360
+ @ability.can :read, :foo
361
+ expect(@ability).to_not have_block(:read, :foo)
362
+ @ability.can :read, :foo do |foo|
363
+ false
364
+ end
365
+ expect(@ability).to have_block(:read, :foo)
366
+ end
367
+
368
+ it "knows when raw sql is used in conditions" do
369
+ @ability.can :read, :foo
370
+ expect(@ability).to_not have_raw_sql(:read, :foo)
371
+ @ability.can :read, :foo, 'false'
372
+ expect(@ability).to have_raw_sql(:read, :foo)
373
+ end
374
+
375
+ it "raises access denied exception with default message if not specified" do
376
+ begin
377
+ @ability.authorize! :read, :foo
378
+ rescue CanCan::AccessDenied => e
379
+ e.default_message = "Access denied!"
380
+ expect(e.message).to eq("Access denied!")
381
+ else
382
+ fail "Expected CanCan::AccessDenied exception to be raised"
383
+ end
384
+ end
385
+
386
+ it "determines model adapterO class by asking AbstractAdapter" do
387
+ adapter_class, model_class = double, double
388
+ allow(CanCan::ModelAdapters::AbstractAdapter).to receive(:adapter_class).with(model_class) { adapter_class }
389
+ allow(adapter_class).to receive(:new).with(model_class, []) { :adapter_instance }
390
+ expect(@ability.model_adapter(model_class, :read)).to eq(:adapter_instance)
391
+ end
392
+
393
+ it "raises an error when attempting to use a block with a hash condition since it's not likely what they want" do
394
+ expect {
395
+ @ability.can :read, Array, :published => true do
396
+ false
397
+ end
398
+ }.to raise_error(CanCan::Error, "You are not able to supply a block with a hash of conditions in read Array ability. Use either one.")
399
+ end
400
+
401
+ describe "unauthorized message" do
402
+ after(:each) do
403
+ I18n.backend = nil
404
+ end
405
+
406
+ it "uses action/subject in i18n" do
407
+ I18n.backend.store_translations :en, :unauthorized => {:update => {:array => "foo"}}
408
+ expect(@ability.unauthorized_message(:update, Array)).to eq("foo")
409
+ expect(@ability.unauthorized_message(:update, [1, 2, 3])).to eq("foo")
410
+ expect(@ability.unauthorized_message(:update, :missing)).to be_nil
411
+ end
412
+
413
+ it "uses symbol as subject directly" do
414
+ I18n.backend.store_translations :en, :unauthorized => {:has => {:cheezburger => "Nom nom nom. I eated it."}}
415
+ expect(@ability.unauthorized_message(:has, :cheezburger)).to eq("Nom nom nom. I eated it.")
416
+ end
417
+
418
+ it "falls back to 'manage' and 'all'" do
419
+ I18n.backend.store_translations :en, :unauthorized => {
420
+ :manage => {:all => "manage all", :array => "manage array"},
421
+ :update => {:all => "update all", :array => "update array"}
422
+ }
423
+ expect(@ability.unauthorized_message(:update, Array)).to eq("update array")
424
+ expect(@ability.unauthorized_message(:update, Hash)).to eq("update all")
425
+ expect(@ability.unauthorized_message(:foo, Array)).to eq("manage array")
426
+ expect(@ability.unauthorized_message(:foo, Hash)).to eq("manage all")
427
+ end
428
+
429
+ it "follows aliased actions" do
430
+ I18n.backend.store_translations :en, :unauthorized => {:modify => {:array => "modify array"}}
431
+ @ability.alias_action :update, :to => :modify
432
+ expect(@ability.unauthorized_message(:update, Array)).to eq("modify array")
433
+ expect(@ability.unauthorized_message(:edit, Array)).to eq("modify array")
434
+ end
435
+
436
+ it "has variables for action and subject" do
437
+ I18n.backend.store_translations :en, :unauthorized => {:manage => {:all => "%{action} %{subject}"}} # old syntax for now in case testing with old I18n
438
+ expect(@ability.unauthorized_message(:update, Array)).to eq("update array")
439
+ expect(@ability.unauthorized_message(:update, ArgumentError)).to eq("update argument error")
440
+ expect(@ability.unauthorized_message(:edit, 1..3)).to eq("edit range")
441
+ end
442
+ end
443
+
444
+ describe "#merge" do
445
+ it "adds the rules from the given ability" do
446
+ @ability.can :use, :tools
447
+ (another_ability = double).extend(CanCan::Ability)
448
+ another_ability.can :use, :search
449
+
450
+ @ability.merge(another_ability)
451
+ expect(@ability.can?(:use, :search)).to be_true
452
+ expect(@ability.send(:rules).size).to eq(2)
453
+ end
454
+ end
455
+ end