codeprimate-cancan 1.6.5

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