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.
- data/Gemfile.lock +1 -1
- data/NEWS.md +34 -0
- data/README.md +14 -0
- data/features/activemodel_integration.feature +15 -0
- data/features/step_definitions/activemodel_steps.rb +21 -0
- data/gemfiles/3.0.gemfile.lock +1 -1
- data/gemfiles/3.1.gemfile.lock +1 -1
- data/gemfiles/3.2.gemfile.lock +1 -1
- data/gemfiles/4.0.0.gemfile.lock +1 -1
- data/gemfiles/4.0.1.gemfile.lock +1 -1
- data/gemfiles/4.1.gemfile.lock +1 -1
- data/lib/shoulda/matchers.rb +1 -0
- data/lib/shoulda/matchers/action_controller/callback_matcher.rb +11 -6
- data/lib/shoulda/matchers/action_controller/strong_parameters_matcher.rb +59 -95
- data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +10 -18
- data/lib/shoulda/matchers/active_model/disallow_value_matcher.rb +10 -0
- data/lib/shoulda/matchers/active_model/ensure_inclusion_of_matcher.rb +60 -18
- data/lib/shoulda/matchers/active_model/errors.rb +9 -7
- data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +4 -0
- data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +24 -5
- data/lib/shoulda/matchers/doublespeak.rb +27 -0
- data/lib/shoulda/matchers/doublespeak/double.rb +74 -0
- data/lib/shoulda/matchers/doublespeak/double_collection.rb +54 -0
- data/lib/shoulda/matchers/doublespeak/double_implementation_registry.rb +27 -0
- data/lib/shoulda/matchers/doublespeak/object_double.rb +32 -0
- data/lib/shoulda/matchers/doublespeak/proxy_implementation.rb +30 -0
- data/lib/shoulda/matchers/doublespeak/structs.rb +8 -0
- data/lib/shoulda/matchers/doublespeak/stub_implementation.rb +34 -0
- data/lib/shoulda/matchers/doublespeak/world.rb +38 -0
- data/lib/shoulda/matchers/independent/delegate_matcher.rb +112 -61
- data/lib/shoulda/matchers/integrations/test_unit.rb +8 -6
- data/lib/shoulda/matchers/rails_shim.rb +16 -0
- data/lib/shoulda/matchers/version.rb +1 -1
- data/spec/shoulda/matchers/action_controller/callback_matcher_spec.rb +22 -19
- data/spec/shoulda/matchers/action_controller/strong_parameters_matcher_spec.rb +174 -65
- data/spec/shoulda/matchers/active_model/allow_value_matcher_spec.rb +14 -0
- data/spec/shoulda/matchers/active_model/ensure_inclusion_of_matcher_spec.rb +553 -211
- data/spec/shoulda/matchers/active_model/numericality_matchers/comparison_matcher_spec.rb +6 -0
- data/spec/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +22 -0
- data/spec/shoulda/matchers/active_model/validate_presence_of_matcher_spec.rb +23 -4
- data/spec/shoulda/matchers/doublespeak/double_collection_spec.rb +102 -0
- data/spec/shoulda/matchers/doublespeak/double_implementation_registry_spec.rb +21 -0
- data/spec/shoulda/matchers/doublespeak/double_spec.rb +144 -0
- data/spec/shoulda/matchers/doublespeak/object_double_spec.rb +77 -0
- data/spec/shoulda/matchers/doublespeak/proxy_implementation_spec.rb +40 -0
- data/spec/shoulda/matchers/doublespeak/stub_implementation_spec.rb +88 -0
- data/spec/shoulda/matchers/doublespeak/world_spec.rb +88 -0
- data/spec/shoulda/matchers/doublespeak_spec.rb +19 -0
- data/spec/shoulda/matchers/independent/delegate_matcher_spec.rb +105 -39
- data/spec/support/controller_builder.rb +18 -9
- data/spec/support/rails_versions.rb +4 -0
- 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
|
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
|
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
|
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
|
198
|
+
case attribute_type
|
192
199
|
when :boolean
|
193
200
|
boolean_outside_values
|
194
|
-
when :
|
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
|
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
|
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
|
7
|
-
|
8
|
-
|
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 :
|
11
|
+
attr_accessor :attribute
|
14
12
|
|
15
13
|
def message
|
16
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
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
|