shoulda-matchers 2.6.0 → 2.6.1.rc1

Sign up to get free protection for your applications and to get access to all the features.
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,320 +1,662 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Shoulda::Matchers::ActiveModel::EnsureInclusionOfMatcher do
4
- context 'with no validations' do
5
- it 'rejects an array which does not have a validator defined' do
6
- expect(define_model(:example, attr: :string).new).
7
- not_to ensure_inclusion_of(:attr).in_array(%w(Yes No))
4
+ shared_context 'for a generic attribute' do
5
+ def self.testing_values_of_option(option_name, &block)
6
+ [nil, true, false].each do |option_value|
7
+ context_name = "+ #{option_name}"
8
+ option_args = []
9
+ matches_or_not = ['matches', 'does not match']
10
+ to_or_not_to = [:to, :not_to]
11
+
12
+ unless option_value == nil
13
+ context_name << "(#{option_value})"
14
+ option_args = [option_value]
15
+ end
16
+
17
+ if option_value == false
18
+ matches_or_not.reverse!
19
+ to_or_not_to.reverse!
20
+ end
21
+
22
+ end
8
23
  end
9
- end
10
24
 
11
- context 'with an integer column' do
12
- it 'can verify a zero in the array' do
13
- model = define_model(:example, attr: :integer) do
14
- validates_inclusion_of :attr, in: [0, 1, 2]
15
- end.new
25
+ context 'against an integer attribute' do
26
+ it_behaves_like 'it supports in_array',
27
+ possible_values: (1..5).to_a,
28
+ zero: 0,
29
+ reserved_outside_value: described_class::ARBITRARY_OUTSIDE_FIXNUM
30
+
31
+ it_behaves_like 'it supports in_range',
32
+ possible_values: 1..5,
33
+ zero: 0
34
+
35
+ context 'when attribute validates a range of values via custom validation' do
36
+ it 'matches ensuring the correct range and messages' do
37
+ expect_to_match_ensuring_range_and_messages(2..5, 2, 5)
38
+ expect_to_match_ensuring_range_and_messages(2...5, 2, 4)
39
+ end
40
+ end
41
+
42
+ def build_object(options = {}, &block)
43
+ build_object_with_generic_attribute(
44
+ options.merge(column_type: :integer, value: 1),
45
+ &block
46
+ )
47
+ end
16
48
 
17
- expect(model).to ensure_inclusion_of(:attr).in_array([0,1,2])
49
+ def add_outside_value_to(values)
50
+ values + [values.last + 1]
51
+ end
18
52
  end
19
- end
20
53
 
21
- context 'with an decimal column' do
22
- it 'can verify decimal values' do
23
- model = define_model(:example, attr: :decimal) do
24
- validates_inclusion_of :attr, in: [0.0, 0.1]
25
- end.new
54
+ context "against a float attribute" do
55
+ it_behaves_like 'it supports in_array',
56
+ possible_values: [1.0, 2.0, 3.0, 4.0, 5.0],
57
+ zero: 0.0,
58
+ reserved_outside_value: described_class::ARBITRARY_OUTSIDE_FIXNUM
59
+
60
+ it_behaves_like 'it supports in_range',
61
+ possible_values: 1.0..5.0,
62
+ zero: 0.0
63
+
64
+ def build_object(options = {}, &block)
65
+ build_object_with_generic_attribute(
66
+ options.merge(column_type: :float, value: 1.0),
67
+ &block
68
+ )
69
+ end
26
70
 
27
- expect(model).to ensure_inclusion_of(:attr).in_array([0.0, 0.1])
71
+ def add_outside_value_to(values)
72
+ values + [values.last + 1]
73
+ end
74
+ end
75
+
76
+ context "against a decimal attribute" do
77
+ it_behaves_like 'it supports in_array',
78
+ possible_values: [1.0, 2.0, 3.0, 4.0, 5.0].map { |number|
79
+ BigDecimal.new(number.to_s)
80
+ },
81
+ zero: BigDecimal.new('0.0'),
82
+ reserved_outside_value: described_class::ARBITRARY_OUTSIDE_DECIMAL
83
+
84
+ it_behaves_like 'it supports in_range',
85
+ possible_values: BigDecimal.new('1.0') .. BigDecimal.new('5.0'),
86
+ zero: BigDecimal.new('0.0')
87
+
88
+ def build_object(options = {}, &block)
89
+ build_object_with_generic_attribute(
90
+ options.merge(column_type: :decimal, value: BigDecimal.new('1.0')),
91
+ &block
92
+ )
93
+ end
94
+
95
+ def add_outside_value_to(values)
96
+ values + [values.last + 1]
97
+ end
98
+ end
99
+
100
+ context 'against a string attribute' do
101
+ it_behaves_like 'it supports in_array',
102
+ possible_values: %w(foo bar baz),
103
+ reserved_outside_value: described_class::ARBITRARY_OUTSIDE_STRING
104
+
105
+ def build_object(options = {}, &block)
106
+ build_object_with_generic_attribute(
107
+ options.merge(column_type: :string),
108
+ &block
109
+ )
110
+ end
111
+
112
+ def add_outside_value_to(values)
113
+ values + %w(qux)
114
+ end
28
115
  end
29
116
  end
30
117
 
31
- context 'with true/false values' do
32
- it 'can verify outside values to ensure the negative case' do
33
- expect(define_model(:example, attr: :string).new).
34
- not_to ensure_inclusion_of(:attr).in_array([true, false])
118
+ shared_examples_for 'it supports allow_nil' do |args|
119
+ valid_values = args.fetch(:valid_values)
120
+
121
+ testing_values_of_option 'allow_nil' do |option_args, matches_or_not, to_or_not_to|
122
+ it "#{matches_or_not[0]} when the validation specifies allow_nil" do
123
+ builder = build_object_allowing(valid_values, allow_nil: true)
124
+
125
+ __send__("expect_#{to_or_not_to[0]}_match_on_values", builder, valid_values) do |matcher|
126
+ matcher.allow_nil(*option_args)
127
+ end
128
+ end
129
+
130
+ it "#{matches_or_not[1]} when the validation does not specify allow_nil" do
131
+ builder = build_object_allowing(valid_values)
132
+
133
+ __send__("expect_#{to_or_not_to[1]}_match_on_values", builder, valid_values) do |matcher|
134
+ matcher.allow_nil(*option_args)
135
+ end
136
+ end
35
137
  end
36
138
  end
37
139
 
38
- context 'where we cannot determine a value outside the array' do
39
- it 'raises a custom exception' do
40
- model = define_model(:example, attr: :string).new
140
+ shared_examples_for 'it supports allow_blank' do |args|
141
+ valid_values = args.fetch(:valid_values)
142
+
143
+ testing_values_of_option 'allow_blank' do |option_args, matches_or_not, to_or_not_to|
144
+ it "#{matches_or_not[0]} when the validation specifies allow_blank" do
145
+ builder = build_object_allowing(valid_values, allow_blank: true)
146
+
147
+ __send__("expect_#{to_or_not_to[0]}_match_on_values", builder, valid_values) do |matcher|
148
+ matcher.allow_blank(*option_args)
149
+ end
150
+ end
151
+
152
+ it "#{matches_or_not[1]} when the validation does not specify allow_blank" do
153
+ builder = build_object_allowing(valid_values)
41
154
 
42
- arbitrary_string = described_class::ARBITRARY_OUTSIDE_STRING
43
- expect { expect(model).to ensure_inclusion_of(:attr).in_array([arbitrary_string]) }.to raise_error Shoulda::Matchers::ActiveModel::CouldNotDetermineValueOutsideOfArray
155
+ __send__("expect_#{to_or_not_to[1]}_match_on_values", builder, valid_values) do |matcher|
156
+ matcher.allow_blank(*option_args)
157
+ end
158
+ end
44
159
  end
45
160
  end
46
161
 
47
- context 'an attribute which must be included in a range' do
48
- it 'accepts ensuring the correct range' do
49
- expect(validating_inclusion(in: 2..5)).
50
- to ensure_inclusion_of(:attr).in_range(2..5)
162
+ shared_examples_for 'it supports with_message' do |args|
163
+ valid_values = args.fetch(:valid_values)
164
+
165
+ context 'given a string' do
166
+ it 'matches when validation uses given message' do
167
+ builder = build_object_allowing(valid_values, message: 'a message')
168
+
169
+ expect_to_match_on_values(builder, valid_values) do |matcher|
170
+ matcher.with_message('a message')
171
+ end
172
+ end
173
+
174
+ it 'does not match when validation uses the default message instead of given message' do
175
+ pending 'does not work'
176
+
177
+ builder = build_object_allowing(valid_values)
178
+
179
+ expect_not_to_match_on_values(builder, valid_values) do |matcher|
180
+ matcher.with_message('a message')
181
+ end
182
+ end
183
+
184
+ it 'does not match when validation uses a message but it is not same as given' do
185
+ pending 'does not work'
186
+
187
+ builder = build_object_allowing(valid_values, message: 'a different message')
188
+
189
+ expect_not_to_match_on_values(builder, valid_values) do |matcher|
190
+ matcher.with_message('a message')
191
+ end
192
+ end
51
193
  end
52
194
 
53
- it 'rejects ensuring a lower minimum value' do
54
- expect(validating_inclusion(in: 2..5)).
55
- not_to ensure_inclusion_of(:attr).in_range(1..5)
195
+ context 'given a regex' do
196
+ it 'matches when validation uses a message that matches the regex' do
197
+ builder = build_object_allowing(valid_values, message: 'this is a message')
198
+
199
+ expect_to_match_on_values(builder, valid_values) do |matcher|
200
+ matcher.with_message(/a message/)
201
+ end
202
+ end
203
+
204
+ it 'does not match when validation uses the default message instead of given message' do
205
+ pending 'does not work'
206
+
207
+ builder = build_object_allowing(valid_values)
208
+
209
+ expect_not_to_match_on_values(builder, valid_values) do |matcher|
210
+ matcher.with_message(/a message/)
211
+ end
212
+ end
213
+
214
+ it 'does not match when validation uses a message but it does not match regex' do
215
+ pending 'does not work'
216
+
217
+ builder = build_object_allowing(valid_values, message: 'a different message')
218
+
219
+ expect_not_to_match_on_values(builder, valid_values) do |matcher|
220
+ matcher.with_message(/a message/)
221
+ end
222
+ end
56
223
  end
57
224
 
58
- it 'rejects ensuring a higher minimum value' do
59
- expect(validating_inclusion(in: 2..5)).
60
- not_to ensure_inclusion_of(:attr).in_range(3..5)
225
+ context 'given nil' do
226
+ it 'is as if with_message had never been called' do
227
+ builder = build_object_allowing(valid_values)
228
+
229
+ expect_to_match_on_values(builder, valid_values) do |matcher|
230
+ matcher.with_message(nil)
231
+ end
232
+ end
61
233
  end
234
+ end
62
235
 
63
- it 'rejects ensuring a lower maximum value' do
64
- expect(validating_inclusion(in: 2..5)).
65
- not_to ensure_inclusion_of(:attr).in_range(2..4)
236
+ shared_examples_for 'it supports in_array' do |args|
237
+ possible_values = args.fetch(:possible_values)
238
+ zero = args[:zero]
239
+ reserved_outside_value = args.fetch(:reserved_outside_value)
240
+
241
+ it 'does not match a record with no validations' do
242
+ builder = build_object
243
+ expect_not_to_match_on_values(builder, possible_values)
66
244
  end
67
245
 
68
- it 'rejects ensuring a higher maximum value' do
69
- expect(validating_inclusion(in: 2..5)).
70
- not_to ensure_inclusion_of(:attr).in_range(2..6)
246
+ it 'matches given the same array of valid values' do
247
+ builder = build_object_allowing(possible_values)
248
+ expect_to_match_on_values(builder, possible_values)
71
249
  end
72
250
 
73
- it 'does not override the default message with a blank' do
74
- expect(validating_inclusion(in: 2..5)).
75
- to ensure_inclusion_of(:attr).in_range(2..5).with_message(nil)
251
+ it 'matches given a subset of the valid values' do
252
+ builder = build_object_allowing(possible_values)
253
+ expect_to_match_on_values(builder, possible_values[1..-1])
76
254
  end
77
- end
78
255
 
79
- context 'an attribute which must be included in a range with excluded end' do
80
- it 'accepts ensuring the correct range' do
81
- expect(validating_inclusion(in: 2...5)).
82
- to ensure_inclusion_of(:attr).in_range(2...5)
256
+ if zero
257
+ it 'matches when one of the given values is a 0' do
258
+ valid_values = possible_values + [zero]
259
+ builder = build_object_allowing(valid_values)
260
+ expect_to_match_on_values(builder, valid_values)
261
+ end
83
262
  end
84
263
 
85
- it 'rejects ensuring a lower maximum value' do
86
- expect(validating_inclusion(in: 2...5)).
87
- not_to ensure_inclusion_of(:attr).in_range(2...4)
264
+ it 'does not match when one of the given values is invalid' do
265
+ builder = build_object_allowing(possible_values)
266
+ expect_not_to_match_on_values(builder, add_outside_value_to(possible_values))
88
267
  end
89
- end
90
268
 
91
- context 'an attribute with a custom ranged value validation' do
92
- it 'accepts ensuring the correct range' do
93
- expect(validating_inclusion(in: 2..4, message: 'not good')).
94
- to ensure_inclusion_of(:attr).in_range(2..4).with_message(/not good/)
269
+ it 'raises an error when valid and given value is our test outside value' do
270
+ error_class = Shoulda::Matchers::ActiveModel::CouldNotDetermineValueOutsideOfArray
271
+ builder = build_object_allowing([reserved_outside_value])
272
+
273
+ expect { expect_to_match_on_values(builder, [reserved_outside_value]) }.
274
+ to raise_error(error_class)
95
275
  end
96
- end
97
276
 
98
- context 'an attribute with custom range validations' do
99
- it 'accepts ensuring the correct range and messages' do
100
- model = custom_validation do
101
- if attr < 2
102
- errors.add(:attr, 'too low')
103
- elsif attr > 5
104
- errors.add(:attr, 'too high')
277
+ it_behaves_like 'it supports allow_nil', valid_values: possible_values
278
+ it_behaves_like 'it supports allow_blank', valid_values: possible_values
279
+ it_behaves_like 'it supports with_message', valid_values: possible_values
280
+
281
+ if active_model_3_2?
282
+ context '+ strict' do
283
+ context 'when the validation specifies strict' do
284
+ it 'matches when the given values match the valid values' do
285
+ builder = build_object_allowing(possible_values, strict: true)
286
+
287
+ expect_to_match_on_values(builder, possible_values) do |matcher|
288
+ matcher.strict
289
+ end
290
+ end
291
+
292
+ it 'does not match when the given values do not match the valid values' do
293
+ builder = build_object_allowing(possible_values, strict: true)
294
+
295
+ values = add_outside_value_to(possible_values)
296
+ expect_not_to_match_on_values(builder, values) do |matcher|
297
+ matcher.strict
298
+ end
299
+ end
105
300
  end
106
- end
107
301
 
108
- expect(model).to ensure_inclusion_of(:attr).in_range(2..5).
109
- with_low_message(/low/).with_high_message(/high/)
302
+ context 'when the validation does not specify strict' do
303
+ it 'does not match' do
304
+ builder = build_object_allowing(possible_values)
110
305
 
111
- model = custom_validation do
112
- if attr < 2
113
- errors.add(:attr, 'too low')
114
- elsif attr > 4
115
- errors.add(:attr, 'too high')
306
+ expect_not_to_match_on_values(builder, possible_values) do |matcher|
307
+ matcher.strict
308
+ end
309
+ end
116
310
  end
117
311
  end
118
-
119
- expect(model).to ensure_inclusion_of(:attr).in_range(2...5).
120
- with_low_message(/low/).with_high_message(/high/)
121
312
  end
122
- end
123
313
 
124
- context 'an attribute which must be included in an array' do
125
- it 'accepts with correct array' do
126
- expect(validating_inclusion(in: %w(one two))).
127
- to ensure_inclusion_of(:attr).in_array(%w(one two))
314
+ def expect_to_match_on_values(builder, values, &block)
315
+ expect_to_match_in_array(builder, values, &block)
128
316
  end
129
317
 
130
- it 'rejects when only part of array matches' do
131
- expect(validating_inclusion(in: %w(one two))).
132
- not_to ensure_inclusion_of(:attr).in_array(%w(one wrong_value))
318
+ def expect_not_to_match_on_values(builder, values, &block)
319
+ expect_not_to_match_in_array(builder, values, &block)
133
320
  end
321
+ end
134
322
 
135
- it 'rejects when array does not match at all' do
136
- expect(validating_inclusion(in: %w(one two))).
137
- not_to ensure_inclusion_of(:attr).in_array(%w(cat dog))
138
- end
323
+ shared_examples_for 'it supports in_range' do |args|
324
+ possible_values = args[:possible_values]
139
325
 
140
- it 'has correct description' do
141
- expect(ensure_inclusion_of(:attr).in_array([true, "dog"]).description).
142
- to eq 'ensure inclusion of attr in [true, "dog"]'
326
+ it 'does not match a record with no validations' do
327
+ builder = build_object
328
+ expect_not_to_match_on_values(builder, possible_values)
143
329
  end
144
330
 
145
- it 'rejects allow_blank' do
146
- expect(validating_inclusion(in: %w(one two))).
147
- not_to ensure_inclusion_of(:attr).in_array(%w(one two)).allow_blank(true)
331
+ it 'matches given a range that exactly matches the valid range' do
332
+ builder = build_object_allowing(possible_values)
333
+ expect_to_match_on_values(builder, possible_values)
148
334
  end
149
335
 
150
- it 'accepts allow_blank(false)' do
151
- expect(validating_inclusion(in: %w(one two))).
152
- to ensure_inclusion_of(:attr).in_array(%w(one two)).allow_blank(false)
336
+ it 'does not match given a range whose start value falls outside valid range' do
337
+ builder = build_object_allowing(possible_values)
338
+ expect_not_to_match_on_values(builder,
339
+ Range.new(possible_values.first - 1, possible_values.last)
340
+ )
153
341
  end
154
342
 
155
- it 'rejects allow_nil' do
156
- expect(validating_inclusion(in: %w(one two))).
157
- not_to ensure_inclusion_of(:attr).in_array(%w(one two)).allow_nil(true)
343
+ it 'does not match given a range whose start value falls inside valid range' do
344
+ builder = build_object_allowing(possible_values)
345
+ expect_not_to_match_on_values(builder,
346
+ Range.new(possible_values.first + 1, possible_values.last)
347
+ )
158
348
  end
159
349
 
160
- it 'accepts allow_nil(false)' do
161
- expect(validating_inclusion(in: %w(one two))).
162
- to ensure_inclusion_of(:attr).in_array(%w(one two)).allow_nil(false)
350
+ it 'does not match given a range whose end value falls inside valid range' do
351
+ builder = build_object_allowing(possible_values)
352
+ expect_not_to_match_on_values(builder,
353
+ Range.new(possible_values.first, possible_values.last - 1)
354
+ )
163
355
  end
164
- end
165
356
 
166
- context 'with allowed blank and allowed nil' do
167
- it 'accepts allow_blank' do
168
- expect(validating_inclusion(in: %w(one two), allow_blank: true)).
169
- to ensure_inclusion_of(:attr).in_array(%w(one two)).allow_blank
357
+ it 'does not match given a range whose end value falls outside valid range' do
358
+ builder = build_object_allowing(possible_values)
359
+ expect_not_to_match_on_values(builder,
360
+ Range.new(possible_values.first, possible_values.last + 1)
361
+ )
170
362
  end
171
363
 
172
- it 'rejects allow_blank(false)' do
173
- expect(validating_inclusion(in: %w(one two), allow_blank: true)).
174
- not_to ensure_inclusion_of(:attr).in_array(%w(one two)).allow_blank(false)
175
- end
364
+ it_behaves_like 'it supports allow_nil', valid_values: possible_values
365
+ it_behaves_like 'it supports allow_blank', valid_values: possible_values
366
+ it_behaves_like 'it supports with_message', valid_values: possible_values
367
+
368
+ if active_model_3_2?
369
+ context '+ strict' do
370
+ context 'when the validation specifies strict' do
371
+ it 'matches when the given range matches the range in the validation' do
372
+ builder = build_object_allowing(possible_values, strict: true)
373
+
374
+ expect_to_match_on_values(builder, possible_values) do |matcher|
375
+ matcher.strict
376
+ end
377
+ end
176
378
 
177
- it 'accepts allow_nil' do
178
- expect(validating_inclusion(in: %w(one two), allow_nil: true)).
179
- to ensure_inclusion_of(:attr).in_array(%w(one two)).allow_nil
379
+ it 'matches when the given range does not match the range in the validation' do
380
+ builder = build_object_allowing(possible_values, strict: true)
381
+
382
+ range = Range.new(possible_values.first, possible_values.last + 1)
383
+ expect_not_to_match_on_values(builder, range) do |matcher|
384
+ matcher.strict
385
+ end
386
+ end
387
+ end
388
+
389
+ context 'when the validation does not specify strict' do
390
+ it 'does not match' do
391
+ builder = build_object_allowing(possible_values)
392
+
393
+ expect_not_to_match_on_values(builder, possible_values) do |matcher|
394
+ matcher.strict
395
+ end
396
+ end
397
+ end
398
+ end
180
399
  end
181
400
 
182
- it 'rejects allow_nil' do
183
- expect(validating_inclusion(in: %w(one two), allow_nil: true)).
184
- not_to ensure_inclusion_of(:attr).in_array(%w(one two)).allow_nil(false)
401
+ def expect_to_match_on_values(builder, range, &block)
402
+ expect_to_match_in_range(builder, range, &block)
185
403
  end
186
- end
187
404
 
188
- context 'an attribute allowing some blank values but not others' do
189
- it 'rejects allow_blank' do
190
- expect(validating_inclusion(in: ['one', 'two', ''])).
191
- not_to ensure_inclusion_of(:attr).in_array(['one', 'two', '']).allow_blank(true)
405
+ def expect_not_to_match_on_values(builder, range, &block)
406
+ expect_not_to_match_in_range(builder, range, &block)
192
407
  end
193
408
  end
194
409
 
195
- if active_model_3_2?
196
- context 'a strict attribute which must be included in a range' do
197
- it 'accepts ensuring the correct range' do
198
- expect(validating_inclusion(in: 2..5, strict: true)).
199
- to ensure_inclusion_of(:attr).in_range(2..5).strict
410
+ shared_context 'against a boolean attribute for true and false' do
411
+ context 'when ensuring inclusion of true' do
412
+ it 'matches' do
413
+ valid_values = [true]
414
+ builder = build_object_allowing(valid_values)
415
+ expect_to_match_in_array(builder, valid_values)
200
416
  end
417
+ end
201
418
 
202
- it 'rejects ensuring another range' do
203
- expect(validating_inclusion(in: 2..5, strict: true)).
204
- not_to ensure_inclusion_of(:attr).in_range(2..6).strict
419
+ context 'when ensuring inclusion of false' do
420
+ it 'matches' do
421
+ valid_values = [false]
422
+ builder = build_object_allowing(valid_values)
423
+ expect_to_match_in_array(builder, valid_values)
205
424
  end
206
425
  end
207
- end
208
426
 
209
- context 'against a boolean attribute' do
210
- context 'which is nullable' do
211
- context 'when ensuring inclusion of true' do
212
- it "doesn't raise an error" do
213
- record = validating_inclusion_of_boolean_in(:attr, [true], null: true)
214
- expect(record).to ensure_inclusion_of(:attr).in_array([true])
427
+ context 'when ensuring inclusion of true and false' do
428
+ it 'matches' do
429
+ valid_values = [true, false]
430
+ builder = build_object_allowing(valid_values)
431
+ capture(:stderr) do
432
+ expect_to_match_in_array(builder, valid_values)
215
433
  end
216
434
  end
217
435
 
218
- context 'when ensuring inclusion of false' do
219
- it "doesn't raise an error" do
220
- record = validating_inclusion_of_boolean_in(:attr, [false], null: true)
221
- expect(record).to ensure_inclusion_of(:attr).in_array([false])
436
+ it 'prints a warning' do
437
+ valid_values = [true, false]
438
+ builder = build_object_allowing(valid_values)
439
+ message = 'You are using `ensure_inclusion_of` to assert that a boolean column allows boolean values and disallows non-boolean ones'
440
+
441
+ stderr = capture(:stderr) do
442
+ expect_to_match_in_array(builder, valid_values)
222
443
  end
444
+
445
+ expect(stderr.gsub(/\n+/, ' ')).to include(message)
223
446
  end
447
+ end
448
+ end
449
+
450
+ context 'for a database column' do
451
+ include_context 'for a generic attribute'
452
+
453
+ context 'against a boolean attribute' do
454
+ context 'which is nullable' do
455
+ include_context 'against a boolean attribute for true and false'
224
456
 
225
- context 'when ensuring inclusion of true and false' do
226
- it "doesn't raise an error" do
227
- record = validating_inclusion_of_boolean_in(:attr, [true, false], null: true)
228
- capture(:stderr) do
229
- expect(record).to ensure_inclusion_of(:attr).in_array([true, false])
457
+ context 'when ensuring inclusion of nil' do
458
+ it 'matches' do
459
+ valid_values = [nil]
460
+ builder = build_object_allowing(valid_values)
461
+ capture(:stderr) do
462
+ expect_to_match_in_array(builder, valid_values)
463
+ end
230
464
  end
231
- end
232
465
 
233
- it 'prints a warning' do
234
- record = validating_inclusion_of_boolean_in(:attr, [true, false], null: true)
235
- stderr = capture(:stderr) do
236
- expect(record).to ensure_inclusion_of(:attr).in_array([true, false])
466
+ it 'prints a warning' do
467
+ valid_values = [nil]
468
+ builder = build_object_allowing(valid_values)
469
+ message = 'You are using `ensure_inclusion_of` to assert that a boolean column allows nil'
470
+
471
+ stderr = capture(:stderr) do
472
+ expect_to_match_in_array(builder, valid_values)
473
+ end
474
+
475
+ expect(stderr.gsub(/\n+/, ' ')).to include(message)
237
476
  end
238
- expect(stderr.gsub(/\n+/, ' ')).
239
- to include('You are using `ensure_inclusion_of` to assert that a boolean column allows boolean values and disallows non-boolean ones')
477
+ end
478
+
479
+ def build_object(options = {}, &block)
480
+ super(options.merge(column_options: { null: true }, value: true))
240
481
  end
241
482
  end
242
483
 
243
- context 'when ensuring inclusion of nil' do
244
- it "doesn't raise an error" do
245
- record = validating_inclusion_of_boolean_in(:attr, [nil], null: true)
246
- capture(:stderr) do
247
- expect(record).to ensure_inclusion_of(:attr).in_array([nil])
484
+ context 'which is non-nullable' do
485
+ include_context 'against a boolean attribute for true and false'
486
+
487
+ context 'when ensuring inclusion of nil' do
488
+ it 'raises a specific error' do
489
+ valid_values = [nil]
490
+ builder = build_object_allowing(valid_values)
491
+ error_class = Shoulda::Matchers::ActiveModel::NonNullableBooleanError
492
+
493
+ expect {
494
+ expect_to_match_in_array(builder, valid_values)
495
+ }.to raise_error(error_class)
248
496
  end
249
497
  end
250
498
 
251
- it 'prints a warning' do
252
- record = validating_inclusion_of_boolean_in(:attr, [nil], null: true)
253
- stderr = capture(:stderr) do
254
- expect(record).to ensure_inclusion_of(:attr).in_array([nil])
255
- end
256
- expect(stderr.gsub(/\n+/, ' ')).
257
- to include('You are using `ensure_inclusion_of` to assert that a boolean column allows nil')
499
+ def build_object(options = {}, &block)
500
+ super(options.merge(column_options: { null: false }))
258
501
  end
259
502
  end
503
+
504
+ def build_object(options = {}, &block)
505
+ build_object_with_generic_attribute(
506
+ options.merge(column_type: :boolean),
507
+ &block
508
+ )
509
+ end
260
510
  end
261
511
 
262
- context 'which is non-nullable' do
263
- context 'when ensuring inclusion of true' do
264
- it "doesn't raise an error" do
265
- record = validating_inclusion_of_boolean_in(:attr, [true], null: false)
266
- expect(record).to ensure_inclusion_of(:attr).in_array([true])
267
- end
512
+
513
+ def build_object_with_generic_attribute(options = {}, &block)
514
+ attribute_name = :attr
515
+ column_type = options.fetch(:column_type)
516
+ column_options = {
517
+ type: column_type,
518
+ options: options.fetch(:column_options, {})
519
+ }
520
+ validation_options = options[:validation_options]
521
+ custom_validation = options[:custom_validation]
522
+
523
+ model = define_model :example, attribute_name => column_options
524
+ customize_model_class(
525
+ model,
526
+ attribute_name,
527
+ validation_options,
528
+ custom_validation
529
+ )
530
+
531
+ object = model.new
532
+
533
+ object_builder_class.new(attribute_name, object, validation_options)
534
+ end
535
+ end
536
+
537
+ context 'for a plain Ruby attribute' do
538
+ include_context 'for a generic attribute'
539
+
540
+ context 'against a boolean attribute (designated by true)' do
541
+ include_context 'against a boolean attribute for true and false'
542
+
543
+ def build_object(options = {}, &block)
544
+ build_object_with_generic_attribute(options.merge(value: true))
268
545
  end
546
+ end
269
547
 
270
- context 'when ensuring inclusion of false' do
271
- it "doesn't raise an error" do
272
- record = validating_inclusion_of_boolean_in(:attr, [false], null: false)
273
- expect(record).to ensure_inclusion_of(:attr).in_array([false])
274
- end
548
+ context 'against a boolean attribute (designated by false)' do
549
+ include_context 'against a boolean attribute for true and false'
550
+
551
+ def build_object(options = {}, &block)
552
+ build_object_with_generic_attribute(options.merge(value: false))
275
553
  end
554
+ end
276
555
 
277
- context 'when ensuring inclusion of true and false' do
278
- it "doesn't raise an error" do
279
- record = validating_inclusion_of_boolean_in(:attr, [true, false], null: false)
280
- capture(:stderr) do
281
- expect(record).to ensure_inclusion_of(:attr).in_array([true, false])
282
- end
283
- end
556
+ def build_object_with_generic_attribute(options = {}, &block)
557
+ attribute_name = :attr
558
+ validation_options = options[:validation_options]
559
+ custom_validation = options[:custom_validation]
560
+ value = options[:value]
284
561
 
285
- it 'prints a warning' do
286
- record = validating_inclusion_of_boolean_in(:attr, [true, false], null: false)
287
- stderr = capture(:stderr) do
288
- expect(record).to ensure_inclusion_of(:attr).in_array([true, false])
289
- end
290
- expect(stderr.gsub(/\n+/, ' ')).
291
- to include('You are using `ensure_inclusion_of` to assert that a boolean column allows boolean values and disallows non-boolean ones')
292
- end
562
+ model = define_active_model_class :example, accessors: [attribute_name]
563
+ customize_model_class(
564
+ model,
565
+ attribute_name,
566
+ validation_options,
567
+ custom_validation
568
+ )
569
+
570
+ object = model.new
571
+ object.__send__("#{attribute_name}=", value)
572
+
573
+ object_builder_class.new(attribute_name, object, validation_options)
574
+ end
575
+ end
576
+
577
+ def object_builder_class
578
+ @_object_builder_class ||= Struct.new(:attribute, :object, :validation_options)
579
+ end
580
+
581
+ def customize_model_class(klass, attribute_name, validation_options, custom_validation)
582
+ klass.class_eval do
583
+ if validation_options
584
+ validates_inclusion_of attribute_name, validation_options
293
585
  end
294
586
 
295
- context 'when ensuring inclusion of nil' do
296
- it 'raises a specific error' do
297
- record = validating_inclusion_of_boolean_in(:attr, [nil], null: false)
298
- error_class = Shoulda::Matchers::ActiveModel::NonNullableBooleanError
299
- expect {
300
- expect(record).to ensure_inclusion_of(:attr).in_array([nil])
301
- }.to raise_error(error_class)
587
+ if custom_validation
588
+ define_method :custom_validation do
589
+ instance_exec(attribute_name, &custom_validation)
302
590
  end
591
+
592
+ validate :custom_validation
303
593
  end
304
594
  end
305
595
  end
306
596
 
307
- def validating_inclusion(options)
308
- define_model(:example, attr: :string) do
309
- validates_inclusion_of :attr, options
310
- end.new
597
+ def build_object_allowing(values, options = {})
598
+ build_object(validation_options: options.merge(in: values))
311
599
  end
312
600
 
313
- def validating_inclusion_of_boolean_in(attribute, values, options = {})
314
- null = options.fetch(:null, true)
315
- column_options = { type: :boolean, options: { null: null } }
316
- define_model(:example, attribute => column_options) do
317
- validates_inclusion_of attribute, in: values
318
- end.new
601
+ def expect_to_match(builder)
602
+ matcher = ensure_inclusion_of(builder.attribute)
603
+ yield matcher if block_given?
604
+ expect(builder.object).to(matcher)
605
+ end
606
+
607
+ def expect_not_to_match(builder)
608
+ matcher = ensure_inclusion_of(builder.attribute)
609
+ yield matcher if block_given?
610
+ expect(builder.object).not_to(matcher)
611
+ end
612
+
613
+ def expect_to_match_in_array(builder, array)
614
+ expect_to_match(builder) do |matcher|
615
+ matcher.in_array(array)
616
+ yield matcher if block_given?
617
+ end
618
+ end
619
+
620
+ def expect_not_to_match_in_array(builder, array)
621
+ expect_not_to_match(builder) do |matcher|
622
+ matcher.in_array(array)
623
+ yield matcher if block_given?
624
+ end
625
+ end
626
+
627
+ def expect_to_match_in_range(builder, range)
628
+ expect_to_match(builder) do |matcher|
629
+ matcher.in_range(range)
630
+ yield matcher if block_given?
631
+ end
632
+ end
633
+
634
+ def expect_not_to_match_in_range(builder, range)
635
+ expect_not_to_match(builder) do |matcher|
636
+ matcher.in_range(range)
637
+ yield matcher if block_given?
638
+ end
639
+ end
640
+
641
+ def expect_to_match_ensuring_range_and_messages(range, low_value, high_value)
642
+ low_message = 'too low'
643
+ high_message = 'too high'
644
+
645
+ builder = build_object custom_validation: ->(attribute) {
646
+ value = __send__(attribute)
647
+
648
+ if value < low_value
649
+ errors.add(attribute, low_message)
650
+ elsif value > high_value
651
+ errors.add(attribute, high_message)
652
+ end
653
+ }
654
+
655
+ expect_to_match(builder) do |matcher|
656
+ matcher.
657
+ in_range(range).
658
+ with_low_message(low_message).
659
+ with_high_message(high_message)
660
+ end
319
661
  end
320
662
  end