shoulda-matchers 2.6.0 → 2.6.1.rc1

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 (52) hide show
  1. data/Gemfile.lock +1 -1
  2. data/NEWS.md +34 -0
  3. data/README.md +14 -0
  4. data/features/activemodel_integration.feature +15 -0
  5. data/features/step_definitions/activemodel_steps.rb +21 -0
  6. data/gemfiles/3.0.gemfile.lock +1 -1
  7. data/gemfiles/3.1.gemfile.lock +1 -1
  8. data/gemfiles/3.2.gemfile.lock +1 -1
  9. data/gemfiles/4.0.0.gemfile.lock +1 -1
  10. data/gemfiles/4.0.1.gemfile.lock +1 -1
  11. data/gemfiles/4.1.gemfile.lock +1 -1
  12. data/lib/shoulda/matchers.rb +1 -0
  13. data/lib/shoulda/matchers/action_controller/callback_matcher.rb +11 -6
  14. data/lib/shoulda/matchers/action_controller/strong_parameters_matcher.rb +59 -95
  15. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +10 -18
  16. data/lib/shoulda/matchers/active_model/disallow_value_matcher.rb +10 -0
  17. data/lib/shoulda/matchers/active_model/ensure_inclusion_of_matcher.rb +60 -18
  18. data/lib/shoulda/matchers/active_model/errors.rb +9 -7
  19. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +4 -0
  20. data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +24 -5
  21. data/lib/shoulda/matchers/doublespeak.rb +27 -0
  22. data/lib/shoulda/matchers/doublespeak/double.rb +74 -0
  23. data/lib/shoulda/matchers/doublespeak/double_collection.rb +54 -0
  24. data/lib/shoulda/matchers/doublespeak/double_implementation_registry.rb +27 -0
  25. data/lib/shoulda/matchers/doublespeak/object_double.rb +32 -0
  26. data/lib/shoulda/matchers/doublespeak/proxy_implementation.rb +30 -0
  27. data/lib/shoulda/matchers/doublespeak/structs.rb +8 -0
  28. data/lib/shoulda/matchers/doublespeak/stub_implementation.rb +34 -0
  29. data/lib/shoulda/matchers/doublespeak/world.rb +38 -0
  30. data/lib/shoulda/matchers/independent/delegate_matcher.rb +112 -61
  31. data/lib/shoulda/matchers/integrations/test_unit.rb +8 -6
  32. data/lib/shoulda/matchers/rails_shim.rb +16 -0
  33. data/lib/shoulda/matchers/version.rb +1 -1
  34. data/spec/shoulda/matchers/action_controller/callback_matcher_spec.rb +22 -19
  35. data/spec/shoulda/matchers/action_controller/strong_parameters_matcher_spec.rb +174 -65
  36. data/spec/shoulda/matchers/active_model/allow_value_matcher_spec.rb +14 -0
  37. data/spec/shoulda/matchers/active_model/ensure_inclusion_of_matcher_spec.rb +553 -211
  38. data/spec/shoulda/matchers/active_model/numericality_matchers/comparison_matcher_spec.rb +6 -0
  39. data/spec/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +22 -0
  40. data/spec/shoulda/matchers/active_model/validate_presence_of_matcher_spec.rb +23 -4
  41. data/spec/shoulda/matchers/doublespeak/double_collection_spec.rb +102 -0
  42. data/spec/shoulda/matchers/doublespeak/double_implementation_registry_spec.rb +21 -0
  43. data/spec/shoulda/matchers/doublespeak/double_spec.rb +144 -0
  44. data/spec/shoulda/matchers/doublespeak/object_double_spec.rb +77 -0
  45. data/spec/shoulda/matchers/doublespeak/proxy_implementation_spec.rb +40 -0
  46. data/spec/shoulda/matchers/doublespeak/stub_implementation_spec.rb +88 -0
  47. data/spec/shoulda/matchers/doublespeak/world_spec.rb +88 -0
  48. data/spec/shoulda/matchers/doublespeak_spec.rb +19 -0
  49. data/spec/shoulda/matchers/independent/delegate_matcher_spec.rb +105 -39
  50. data/spec/support/controller_builder.rb +18 -9
  51. data/spec/support/rails_versions.rb +4 -0
  52. metadata +34 -8
@@ -1,7 +1,13 @@
1
+ require 'forwardable'
2
+
1
3
  module Shoulda # :nodoc:
2
4
  module Matchers
3
5
  module ActiveModel # :nodoc:
4
6
  class DisallowValueMatcher # :nodoc:
7
+ extend Forwardable
8
+
9
+ def_delegators :allow_matcher, :_after_setting_value
10
+
5
11
  def initialize(value)
6
12
  @allow_matcher = AllowValueMatcher.new(value)
7
13
  end
@@ -39,6 +45,10 @@ module Shoulda # :nodoc:
39
45
  @allow_matcher.strict
40
46
  self
41
47
  end
48
+
49
+ private
50
+
51
+ attr_reader :allow_matcher
42
52
  end
43
53
  end
44
54
  end
@@ -1,3 +1,5 @@
1
+ require 'bigdecimal'
2
+
1
3
  module Shoulda # :nodoc:
2
4
  module Matchers
3
5
  module ActiveModel # :nodoc:
@@ -24,7 +26,7 @@ module Shoulda # :nodoc:
24
26
  class EnsureInclusionOfMatcher < ValidationMatcher # :nodoc:
25
27
  ARBITRARY_OUTSIDE_STRING = 'shouldamatchersteststring'
26
28
  ARBITRARY_OUTSIDE_FIXNUM = 123456789
27
- ARBITRARY_OUTSIDE_DECIMAL = 0.123456789
29
+ ARBITRARY_OUTSIDE_DECIMAL = BigDecimal.new('0.123456789')
28
30
  BOOLEAN_ALLOWS_BOOLEAN_MESSAGE = <<EOT
29
31
  You are using `ensure_inclusion_of` to assert that a boolean column allows
30
32
  boolean values and disallows non-boolean ones. Assuming you are using
@@ -38,11 +40,6 @@ You are using `ensure_inclusion_of` to assert that a boolean column allows nil.
38
40
  Be aware that it is not possible to fully test this, as anything other than
39
41
  true, false or nil will be converted to false. Hence, you should consider
40
42
  removing this test and the corresponding validation.
41
- EOT
42
- BOOLEAN_ALLOWS_NIL_WITH_NOT_NULL_MESSAGE = <<EOT
43
- You have specified that your model's #{@attribute} should ensure inclusion of nil.
44
- However, #{@attribute} is a boolean column which does not allow null values.
45
- Hence, this test will fail and there is no way to make it pass.
46
43
  EOT
47
44
 
48
45
  def initialize(attribute)
@@ -100,13 +97,9 @@ EOT
100
97
  if @range
101
98
  @low_message ||= :inclusion
102
99
  @high_message ||= :inclusion
103
-
104
- disallows_lower_value &&
105
- allows_minimum_value &&
106
- disallows_higher_value &&
107
- allows_maximum_value
100
+ matches_for_range?
108
101
  elsif @array
109
- if allows_all_values_in_array? && allows_blank_value? && allows_nil_value? && disallows_value_outside_of_array?
102
+ if matches_for_array?
110
103
  true
111
104
  else
112
105
  @failure_message = "#{@array} doesn't match array in validation"
@@ -117,6 +110,20 @@ EOT
117
110
 
118
111
  private
119
112
 
113
+ def matches_for_range?
114
+ disallows_lower_value &&
115
+ allows_minimum_value &&
116
+ disallows_higher_value &&
117
+ allows_maximum_value
118
+ end
119
+
120
+ def matches_for_array?
121
+ allows_all_values_in_array? &&
122
+ allows_blank_value? &&
123
+ allows_nil_value? &&
124
+ disallows_value_outside_of_array?
125
+ end
126
+
120
127
  def allows_blank_value?
121
128
  if @options.key?(:allow_blank)
122
129
  blank_values = ['', ' ', "\n", "\r", "\t", "\f"]
@@ -161,7 +168,7 @@ EOT
161
168
  end
162
169
 
163
170
  def disallows_value_outside_of_array?
164
- if attribute_column.type == :boolean
171
+ if attribute_type == :boolean
165
172
  case @array
166
173
  when [true, false]
167
174
  Shoulda::Matchers.warn BOOLEAN_ALLOWS_BOOLEAN_MESSAGE
@@ -171,7 +178,7 @@ EOT
171
178
  Shoulda::Matchers.warn BOOLEAN_ALLOWS_NIL_MESSAGE
172
179
  return true
173
180
  else
174
- raise NonNullableBooleanError, BOOLEAN_ALLOWS_NIL_WITH_NOT_NULL_MESSAGE
181
+ raise NonNullableBooleanError.create(@attribute)
175
182
  end
176
183
  end
177
184
  end
@@ -188,10 +195,10 @@ EOT
188
195
  end
189
196
 
190
197
  def outside_values
191
- case attribute_column.type
198
+ case attribute_type
192
199
  when :boolean
193
200
  boolean_outside_values
194
- when :integer, :float
201
+ when :fixnum
195
202
  [ARBITRARY_OUTSIDE_FIXNUM]
196
203
  when :decimal
197
204
  [ARBITRARY_OUTSIDE_DECIMAL]
@@ -209,15 +216,50 @@ EOT
209
216
  else raise CouldNotDetermineValueOutsideOfArray
210
217
  end
211
218
 
212
- if attribute_column.null
219
+ if attribute_allows_nil?
213
220
  values << nil
214
221
  end
215
222
 
216
223
  values
217
224
  end
218
225
 
226
+ def attribute_type
227
+ if attribute_column
228
+ column_type_to_attribute_type(attribute_column.type)
229
+ else
230
+ value_to_attribute_type(@subject.__send__(@attribute))
231
+ end
232
+ end
233
+
234
+ def attribute_allows_nil?
235
+ if attribute_column
236
+ attribute_column.null
237
+ else
238
+ true
239
+ end
240
+ end
241
+
219
242
  def attribute_column
220
- @subject.class.columns_hash[@attribute.to_s]
243
+ if @subject.class.respond_to?(:columns_hash)
244
+ @subject.class.columns_hash[@attribute.to_s]
245
+ end
246
+ end
247
+
248
+ def column_type_to_attribute_type(type)
249
+ case type
250
+ when :boolean, :decimal then type
251
+ when :integer, :float then :fixnum
252
+ else :default
253
+ end
254
+ end
255
+
256
+ def value_to_attribute_type(value)
257
+ case value
258
+ when true, false then :boolean
259
+ when BigDecimal then :decimal
260
+ when Fixnum then :fixnum
261
+ else :default
262
+ end
221
263
  end
222
264
  end
223
265
  end
@@ -3,17 +3,19 @@ module Shoulda # :nodoc:
3
3
  module ActiveModel # :nodoc:
4
4
  class CouldNotDetermineValueOutsideOfArray < RuntimeError; end
5
5
 
6
- class NonNullableBooleanError < Shoulda::Matchers::Error; end
7
-
8
- class CouldNotClearAttribute < Shoulda::Matchers::Error
9
- def self.create(actual_value)
10
- super(actual_value: actual_value)
6
+ class NonNullableBooleanError < Shoulda::Matchers::Error
7
+ def self.create(attribute)
8
+ super(attribute: attribute)
11
9
  end
12
10
 
13
- attr_accessor :actual_value
11
+ attr_accessor :attribute
14
12
 
15
13
  def message
16
- "Expected value to be nil, but was #{actual_value.inspect}."
14
+ <<-EOT.strip
15
+ You have specified that your model's #{attribute} should ensure inclusion of nil.
16
+ However, #{attribute} is a boolean column which does not allow null values.
17
+ Hence, this test will fail and there is no way to make it pass.
18
+ EOT
17
19
  end
18
20
  end
19
21
 
@@ -7,6 +7,9 @@ module Shoulda # :nodoc:
7
7
  # is_greater_than(6).
8
8
  # less_than(20)...(and so on) }
9
9
  class ComparisonMatcher < ValidationMatcher
10
+ DIFF_TO_COMPARE_PRECISION_CHANGE_NUMBER = 2**34
11
+ attr_reader :diff_to_compare
12
+
10
13
  def initialize(numericality_matcher, value, operator)
11
14
  unless numericality_matcher.respond_to? :diff_to_compare
12
15
  raise ArgumentError, 'numericality_matcher is invalid'
@@ -15,6 +18,7 @@ module Shoulda # :nodoc:
15
18
  @value = value
16
19
  @operator = operator
17
20
  @message = nil
21
+ @diff_to_compare = value.abs < DIFF_TO_COMPARE_PRECISION_CHANGE_NUMBER ? 0.000_001 : 1
18
22
  end
19
23
 
20
24
  def for(attribute)
@@ -28,12 +28,11 @@ module Shoulda # :nodoc:
28
28
  def matches?(subject)
29
29
  super(subject)
30
30
  @expected_message ||= :blank
31
- disallows_value_of(blank_value, @expected_message)
32
- rescue Shoulda::Matchers::ActiveModel::CouldNotClearAttribute => error
33
- if @attribute == :password
34
- raise Shoulda::Matchers::ActiveModel::CouldNotSetPasswordError.create(subject.class)
31
+
32
+ if secure_password_being_validated?
33
+ disallows_and_double_checks_value_of!(blank_value, @expected_message)
35
34
  else
36
- raise error
35
+ disallows_value_of(blank_value, @expected_message)
37
36
  end
38
37
  end
39
38
 
@@ -43,6 +42,26 @@ module Shoulda # :nodoc:
43
42
 
44
43
  private
45
44
 
45
+ def secure_password_being_validated?
46
+ defined?(::ActiveModel::SecurePassword) &&
47
+ @subject.class.ancestors.include?(::ActiveModel::SecurePassword::InstanceMethodsOnActivation) &&
48
+ @attribute == :password
49
+ end
50
+
51
+ def disallows_and_double_checks_value_of!(value, message)
52
+ error_class = Shoulda::Matchers::ActiveModel::CouldNotSetPasswordError
53
+
54
+ disallows_value_of(value, message) do |matcher|
55
+ matcher._after_setting_value do
56
+ actual_value = @subject.__send__(@attribute)
57
+
58
+ if !actual_value.nil?
59
+ raise error_class.create(@subject.class)
60
+ end
61
+ end
62
+ end
63
+ end
64
+
46
65
  def blank_value
47
66
  if collection?
48
67
  []
@@ -0,0 +1,27 @@
1
+ require 'forwardable'
2
+
3
+ module Shoulda
4
+ module Matchers
5
+ module Doublespeak
6
+ class << self
7
+ extend Forwardable
8
+
9
+ def_delegators :world, :register_double_collection,
10
+ :with_doubles_activated
11
+
12
+ def world
13
+ @_world ||= World.new
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ require 'shoulda/matchers/doublespeak/double'
21
+ require 'shoulda/matchers/doublespeak/double_collection'
22
+ require 'shoulda/matchers/doublespeak/double_implementation_registry'
23
+ require 'shoulda/matchers/doublespeak/object_double'
24
+ require 'shoulda/matchers/doublespeak/proxy_implementation'
25
+ require 'shoulda/matchers/doublespeak/structs'
26
+ require 'shoulda/matchers/doublespeak/stub_implementation'
27
+ require 'shoulda/matchers/doublespeak/world'
@@ -0,0 +1,74 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module Doublespeak
4
+ class Double
5
+ attr_reader :calls
6
+
7
+ def initialize(klass, method_name, implementation)
8
+ @klass = klass
9
+ @method_name = method_name
10
+ @implementation = implementation
11
+ @activated = false
12
+ @calls = []
13
+ end
14
+
15
+ def to_return(value = nil, &block)
16
+ if block
17
+ implementation.returns(&block)
18
+ else
19
+ implementation.returns(value)
20
+ end
21
+ end
22
+
23
+ def activate
24
+ unless @activated
25
+ store_original_method
26
+ replace_method_with_double
27
+ @activated = true
28
+ end
29
+ end
30
+
31
+ def deactivate
32
+ if @activated
33
+ restore_original_method
34
+ @activated = false
35
+ end
36
+ end
37
+
38
+ def record_call(args, block)
39
+ calls << MethodCall.new(args, block)
40
+ end
41
+
42
+ def call_original_method(object, args, block)
43
+ if original_method
44
+ original_method.bind(object).call(*args, &block)
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :klass, :method_name, :implementation, :original_method
51
+
52
+ def store_original_method
53
+ @original_method = klass.instance_method(method_name)
54
+ end
55
+
56
+ def replace_method_with_double
57
+ implementation = @implementation
58
+ double = self
59
+
60
+ klass.__send__(:define_method, method_name) do |*args, &block|
61
+ implementation.call(double, self, args, block)
62
+ end
63
+ end
64
+
65
+ def restore_original_method
66
+ original_method = @original_method
67
+ klass.__send__(:define_method, method_name) do |*args, &block|
68
+ original_method.bind(self).call(*args, &block)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,54 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module Doublespeak
4
+ class DoubleCollection
5
+ def initialize(klass)
6
+ @klass = klass
7
+ @doubles_by_method_name = {}
8
+ end
9
+
10
+ def register_stub(method_name)
11
+ register_double(method_name, :stub)
12
+ end
13
+
14
+ def register_proxy(method_name)
15
+ register_double(method_name, :proxy)
16
+ end
17
+
18
+ def activate
19
+ doubles_by_method_name.each do |method_name, double|
20
+ double.activate
21
+ end
22
+ end
23
+
24
+ def deactivate
25
+ doubles_by_method_name.each do |method_name, double|
26
+ double.deactivate
27
+ end
28
+ end
29
+
30
+ def calls_to(method_name)
31
+ double = doubles_by_method_name[method_name]
32
+
33
+ if double
34
+ double.calls
35
+ else
36
+ []
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :klass, :doubles_by_method_name
43
+
44
+ def register_double(method_name, implementation_type)
45
+ implementation =
46
+ DoubleImplementationRegistry.find(implementation_type)
47
+ double = Double.new(klass, method_name, implementation)
48
+ doubles_by_method_name[method_name] = double
49
+ double
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,27 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module Doublespeak
4
+ module DoubleImplementationRegistry
5
+ class << self
6
+ REGISTRY = {}
7
+
8
+ def find(type)
9
+ find_class!(type).create
10
+ end
11
+
12
+ def register(klass, type)
13
+ REGISTRY[type] = klass
14
+ end
15
+
16
+ private
17
+
18
+ def find_class!(type)
19
+ REGISTRY.fetch(type) do
20
+ raise ArgumentError, "No double implementation class found for '#{type}'"
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end