shoulda-matchers 5.3.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +22 -7
  4. data/lib/shoulda/matchers/action_controller/permit_matcher.rb +1 -1
  5. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +9 -0
  6. data/lib/shoulda/matchers/active_model/comparison_matcher.rb +162 -0
  7. data/lib/shoulda/matchers/active_model/numericality_matchers/range_matcher.rb +1 -1
  8. data/lib/shoulda/matchers/active_model/numericality_matchers/submatchers.rb +21 -6
  9. data/lib/shoulda/matchers/active_model/validate_comparison_of_matcher.rb +534 -0
  10. data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +3 -3
  11. data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +4 -3
  12. data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +64 -9
  13. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +32 -86
  14. data/lib/shoulda/matchers/active_model/validation_matcher.rb +6 -0
  15. data/lib/shoulda/matchers/active_model/validator.rb +4 -0
  16. data/lib/shoulda/matchers/active_model.rb +2 -1
  17. data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +0 -8
  18. data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +46 -6
  19. data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +14 -3
  20. data/lib/shoulda/matchers/active_record/have_implicit_order_column.rb +3 -5
  21. data/lib/shoulda/matchers/active_record/normalize_matcher.rb +151 -0
  22. data/lib/shoulda/matchers/active_record.rb +1 -0
  23. data/lib/shoulda/matchers/independent/delegate_method_matcher.rb +1 -1
  24. data/lib/shoulda/matchers/rails_shim.rb +8 -6
  25. data/lib/shoulda/matchers/util/word_wrap.rb +1 -1
  26. data/lib/shoulda/matchers/util.rb +1 -1
  27. data/lib/shoulda/matchers/version.rb +1 -1
  28. data/lib/shoulda/matchers.rb +2 -2
  29. data/shoulda-matchers.gemspec +1 -1
  30. metadata +10 -8
  31. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +0 -136
@@ -3,7 +3,7 @@ module Shoulda
3
3
  module ActiveModel
4
4
  # The `validate_length_of` matcher tests usage of the
5
5
  # `validates_length_of` matcher. Note that this matcher is intended to be
6
- # used against string columns and not integer columns.
6
+ # used against string columns and associations and not integer columns.
7
7
  #
8
8
  # #### Qualifiers
9
9
  #
@@ -36,7 +36,8 @@ module Shoulda
36
36
  #
37
37
  # Use `is_at_least` to test usage of the `:minimum` option. This asserts
38
38
  # that the attribute can take a string which is equal to or longer than
39
- # the given length and cannot take a string which is shorter.
39
+ # the given length and cannot take a string which is shorter. This qualifier
40
+ # also works for associations.
40
41
  #
41
42
  # class User
42
43
  # include ActiveModel::Model
@@ -61,7 +62,8 @@ module Shoulda
61
62
  #
62
63
  # Use `is_at_most` to test usage of the `:maximum` option. This asserts
63
64
  # that the attribute can take a string which is equal to or shorter than
64
- # the given length and cannot take a string which is longer.
65
+ # the given length and cannot take a string which is longer. This qualifier
66
+ # also works for associations.
65
67
  #
66
68
  # class User
67
69
  # include ActiveModel::Model
@@ -84,7 +86,8 @@ module Shoulda
84
86
  #
85
87
  # Use `is_equal_to` to test usage of the `:is` option. This asserts that
86
88
  # the attribute can take a string which is exactly equal to the given
87
- # length and cannot take a string which is shorter or longer.
89
+ # length and cannot take a string which is shorter or longer. This qualifier
90
+ # also works for associations.
88
91
  #
89
92
  # class User
90
93
  # include ActiveModel::Model
@@ -106,7 +109,7 @@ module Shoulda
106
109
  # ##### is_at_least + is_at_most
107
110
  #
108
111
  # Use `is_at_least` and `is_at_most` together to test usage of the `:in`
109
- # option.
112
+ # option. This qualifies also works for associations.
110
113
  #
111
114
  # class User
112
115
  # include ActiveModel::Model
@@ -260,6 +263,29 @@ module Shoulda
260
263
  # should validate_length_of(:bio).is_at_least(15).allow_blank
261
264
  # end
262
265
  #
266
+ # ##### as_array
267
+ #
268
+ # Use `as_array` if you have an ActiveModel model and the attribute being validated
269
+ # is designed to store an array. (This is not necessary if you have an ActiveRecord
270
+ # model with an array column, as the matcher will detect this automatically.)
271
+ #
272
+ # class User
273
+ # include ActiveModel::Model
274
+ # attribute :arr, array: true
275
+ #
276
+ # validates_length_of :arr, minimum: 15
277
+ # end
278
+ #
279
+ # # RSpec
280
+ # describe User do
281
+ # it { should validate_length_of(:arr).as_array.is_at_least(15) }
282
+ # end
283
+ #
284
+ # # Minitest (Shoulda)
285
+ # class UserTest < ActiveSupport::TestCase
286
+ # should validate_length_of(:arr).as_array.is_at_least(15)
287
+ # end
288
+ #
263
289
  def validate_length_of(attr)
264
290
  ValidateLengthOfMatcher.new(attr)
265
291
  end
@@ -275,6 +301,11 @@ module Shoulda
275
301
  @long_message = nil
276
302
  end
277
303
 
304
+ def as_array
305
+ @options[:array] = true
306
+ self
307
+ end
308
+
278
309
  def is_at_least(length)
279
310
  @options[:minimum] = length
280
311
  @short_message ||= :too_short
@@ -451,15 +482,39 @@ module Shoulda
451
482
  end
452
483
 
453
484
  def allows_length_of?(length, message)
454
- allows_value_of(string_of_length(length), message)
485
+ allows_value_of(value_of_length(length), message)
455
486
  end
456
487
 
457
488
  def disallows_length_of?(length, message)
458
- disallows_value_of(string_of_length(length), message)
489
+ disallows_value_of(value_of_length(length), message)
490
+ end
491
+
492
+ def value_of_length(length)
493
+ if array_column?
494
+ ['x'] * length
495
+ elsif collection_association?
496
+ Array.new(length) { association_reflection.klass.new }
497
+ else
498
+ 'x' * length
499
+ end
500
+ end
501
+
502
+ def array_column?
503
+ @options[:array] || super
504
+ end
505
+
506
+ def collection_association?
507
+ association? && [:has_many, :has_and_belongs_to_many].include?(
508
+ association_reflection.macro,
509
+ )
510
+ end
511
+
512
+ def association?
513
+ association_reflection.present?
459
514
  end
460
515
 
461
- def string_of_length(length)
462
- 'x' * length
516
+ def association_reflection
517
+ model.try(:reflect_on_association, @attribute)
463
518
  end
464
519
 
465
520
  def translated_short_message
@@ -357,51 +357,33 @@ module Shoulda
357
357
  end
358
358
 
359
359
  # @private
360
- class ValidateNumericalityOfMatcher
360
+ class ValidateNumericalityOfMatcher < ValidationMatcher
361
361
  NUMERIC_NAME = 'number'.freeze
362
362
  DEFAULT_DIFF_TO_COMPARE = 1
363
363
 
364
- include Qualifiers::IgnoringInterferenceByWriter
365
-
366
364
  attr_reader :diff_to_compare
367
365
 
368
366
  def initialize(attribute)
369
367
  super
370
- @attribute = attribute
371
368
  @submatchers = []
372
369
  @diff_to_compare = DEFAULT_DIFF_TO_COMPARE
373
- @expects_custom_validation_message = false
374
370
  @expects_to_allow_nil = false
375
- @expects_strict = false
376
371
  @allowed_type_adjective = nil
377
372
  @allowed_type_name = 'number'
378
- @context = nil
379
- @expected_message = nil
380
- end
381
373
 
382
- def strict
383
- @expects_strict = true
384
- self
385
- end
386
-
387
- def expects_strict?
388
- @expects_strict
374
+ add_disallow_non_numeric_value_matcher
389
375
  end
390
376
 
391
377
  def only_integer
392
378
  prepare_submatcher(
393
- NumericalityMatchers::OnlyIntegerMatcher.new(self, @attribute),
379
+ NumericalityMatchers::OnlyIntegerMatcher.new(self, attribute),
394
380
  )
395
381
  self
396
382
  end
397
383
 
398
384
  def allow_nil
399
385
  @expects_to_allow_nil = true
400
- prepare_submatcher(
401
- AllowValueMatcher.new(nil).
402
- for(@attribute).
403
- with_message(:not_a_number),
404
- )
386
+ prepare_submatcher(allow_value_matcher(nil, :not_a_number))
405
387
  self
406
388
  end
407
389
 
@@ -411,70 +393,55 @@ module Shoulda
411
393
 
412
394
  def odd
413
395
  prepare_submatcher(
414
- NumericalityMatchers::OddNumberMatcher.new(self, @attribute),
396
+ NumericalityMatchers::OddNumberMatcher.new(self, attribute),
415
397
  )
416
398
  self
417
399
  end
418
400
 
419
401
  def even
420
402
  prepare_submatcher(
421
- NumericalityMatchers::EvenNumberMatcher.new(self, @attribute),
403
+ NumericalityMatchers::EvenNumberMatcher.new(self, attribute),
422
404
  )
423
405
  self
424
406
  end
425
407
 
426
408
  def is_greater_than(value)
427
- prepare_submatcher(comparison_matcher_for(value, :>).for(@attribute))
409
+ prepare_submatcher(comparison_matcher_for(value, :>).for(attribute))
428
410
  self
429
411
  end
430
412
 
431
413
  def is_greater_than_or_equal_to(value)
432
- prepare_submatcher(comparison_matcher_for(value, :>=).for(@attribute))
414
+ prepare_submatcher(comparison_matcher_for(value, :>=).for(attribute))
433
415
  self
434
416
  end
435
417
 
436
418
  def is_equal_to(value)
437
- prepare_submatcher(comparison_matcher_for(value, :==).for(@attribute))
419
+ prepare_submatcher(comparison_matcher_for(value, :==).for(attribute))
438
420
  self
439
421
  end
440
422
 
441
423
  def is_less_than(value)
442
- prepare_submatcher(comparison_matcher_for(value, :<).for(@attribute))
424
+ prepare_submatcher(comparison_matcher_for(value, :<).for(attribute))
443
425
  self
444
426
  end
445
427
 
446
428
  def is_less_than_or_equal_to(value)
447
- prepare_submatcher(comparison_matcher_for(value, :<=).for(@attribute))
429
+ prepare_submatcher(comparison_matcher_for(value, :<=).for(attribute))
448
430
  self
449
431
  end
450
432
 
451
433
  def is_other_than(value)
452
- prepare_submatcher(comparison_matcher_for(value, :!=).for(@attribute))
434
+ prepare_submatcher(comparison_matcher_for(value, :!=).for(attribute))
453
435
  self
454
436
  end
455
437
 
456
438
  def is_in(range)
457
439
  prepare_submatcher(
458
- NumericalityMatchers::RangeMatcher.new(self, @attribute, range),
440
+ NumericalityMatchers::RangeMatcher.new(self, attribute, range),
459
441
  )
460
442
  self
461
443
  end
462
444
 
463
- def with_message(message)
464
- @expects_custom_validation_message = true
465
- @expected_message = message
466
- self
467
- end
468
-
469
- def expects_custom_validation_message?
470
- @expects_custom_validation_message
471
- end
472
-
473
- def on(context)
474
- @context = context
475
- self
476
- end
477
-
478
445
  def matches?(subject)
479
446
  matches_or_does_not_match?(subject)
480
447
  first_submatcher_that_fails_to_match.nil?
@@ -488,7 +455,7 @@ module Shoulda
488
455
  def simple_description
489
456
  description = ''
490
457
 
491
- description << "validate that :#{@attribute} looks like "
458
+ description << "validate that :#{attribute} looks like "
492
459
  description << Shoulda::Matchers::Util.a_or_an(full_allowed_type)
493
460
 
494
461
  if range_description.present?
@@ -502,10 +469,6 @@ module Shoulda
502
469
  description
503
470
  end
504
471
 
505
- def description
506
- ValidationMatcher::BuildDescription.call(self, simple_description)
507
- end
508
-
509
472
  def failure_message
510
473
  overall_failure_message.dup.tap do |message|
511
474
  message << "\n"
@@ -532,44 +495,29 @@ module Shoulda
532
495
  @subject = subject
533
496
  @number_of_submatchers = @submatchers.size
534
497
 
535
- add_disallow_value_matcher
536
498
  qualify_submatchers
537
499
  end
538
500
 
539
- def overall_failure_message
540
- Shoulda::Matchers.word_wrap(
541
- "Expected #{model.name} to #{description}, but this could not "\
542
- 'be proved.',
543
- )
544
- end
545
-
546
- def overall_failure_message_when_negated
547
- Shoulda::Matchers.word_wrap(
548
- "Expected #{model.name} not to #{description}, but this could not "\
549
- 'be proved.',
550
- )
551
- end
552
-
553
501
  def attribute_is_active_record_column?
554
- columns_hash.key?(@attribute.to_s)
502
+ columns_hash.key?(attribute.to_s)
555
503
  end
556
504
 
557
505
  def column_type
558
- columns_hash[@attribute.to_s].type
506
+ columns_hash[attribute.to_s].type
559
507
  end
560
508
 
561
509
  def columns_hash
562
- if @subject.class.respond_to?(:columns_hash)
563
- @subject.class.columns_hash
510
+ if subject.class.respond_to?(:columns_hash)
511
+ subject.class.columns_hash
564
512
  else
565
513
  {}
566
514
  end
567
515
  end
568
516
 
569
- def add_disallow_value_matcher
517
+ def add_disallow_non_numeric_value_matcher
570
518
  disallow_value_matcher = DisallowValueMatcher.
571
519
  new(non_numeric_value).
572
- for(@attribute).
520
+ for(attribute).
573
521
  with_message(:not_a_number)
574
522
 
575
523
  add_submatcher(disallow_value_matcher)
@@ -581,9 +529,9 @@ module Shoulda
581
529
  end
582
530
 
583
531
  def comparison_matcher_for(value, operator)
584
- NumericalityMatchers::ComparisonMatcher.
532
+ ComparisonMatcher.
585
533
  new(self, value, operator).
586
- for(@attribute)
534
+ for(attribute)
587
535
  end
588
536
 
589
537
  def add_submatcher(submatcher)
@@ -615,8 +563,8 @@ module Shoulda
615
563
  submatcher.with_message(@expected_message)
616
564
  end
617
565
 
618
- if @context
619
- submatcher.on(@context)
566
+ if context
567
+ submatcher.on(context)
620
568
  end
621
569
 
622
570
  submatcher.ignoring_interference_by_writer(
@@ -634,23 +582,25 @@ module Shoulda
634
582
  end
635
583
 
636
584
  def has_been_qualified?
637
- @submatchers.any? do |submatcher|
638
- Shoulda::Matchers::RailsShim.parent_of(submatcher.class) ==
639
- NumericalityMatchers
640
- end
585
+ @submatchers.any? { |submatcher| submatcher_qualified?(submatcher) }
586
+ end
587
+
588
+ def submatcher_qualified?(submatcher)
589
+ Shoulda::Matchers::RailsShim.parent_of(submatcher.class) ==
590
+ NumericalityMatchers || submatcher.instance_of?(ComparisonMatcher)
641
591
  end
642
592
 
643
593
  def first_submatcher_that_fails_to_match
644
594
  @_first_submatcher_that_fails_to_match ||=
645
595
  @submatchers.detect do |submatcher|
646
- !submatcher.matches?(@subject)
596
+ !submatcher.matches?(subject)
647
597
  end
648
598
  end
649
599
 
650
600
  def first_submatcher_that_fails_to_not_match
651
601
  @_first_submatcher_that_fails_to_not_match ||=
652
602
  @submatchers.detect do |submatcher|
653
- !submatcher.does_not_match?(@subject)
603
+ !submatcher.does_not_match?(subject)
654
604
  end
655
605
  end
656
606
 
@@ -719,10 +669,6 @@ module Shoulda
719
669
  range_submatcher&.range_description
720
670
  end
721
671
 
722
- def model
723
- @subject.class
724
- end
725
-
726
672
  def non_numeric_value
727
673
  'abcd'
728
674
  end
@@ -186,6 +186,12 @@ module Shoulda
186
186
  def blank_values
187
187
  ['', ' ', "\n", "\r", "\t", "\f"]
188
188
  end
189
+
190
+ def array_column?
191
+ @subject.class.respond_to?(:columns_hash) &&
192
+ @subject.class.columns_hash[@attribute.to_s].respond_to?(:array) &&
193
+ @subject.class.columns_hash[@attribute.to_s].array
194
+ end
189
195
  end
190
196
  end
191
197
  end
@@ -25,6 +25,10 @@ module Shoulda
25
25
  messages.any?
26
26
  end
27
27
 
28
+ def has_any_errors?
29
+ record.errors.any?
30
+ end
31
+
28
32
  def captured_validation_exception?
29
33
  @captured_validation_exception
30
34
  end
@@ -21,8 +21,9 @@ require 'shoulda/matchers/active_model/validate_presence_of_matcher'
21
21
  require 'shoulda/matchers/active_model/validate_acceptance_of_matcher'
22
22
  require 'shoulda/matchers/active_model/validate_confirmation_of_matcher'
23
23
  require 'shoulda/matchers/active_model/validate_numericality_of_matcher'
24
+ require 'shoulda/matchers/active_model/validate_comparison_of_matcher'
25
+ require 'shoulda/matchers/active_model/comparison_matcher'
24
26
  require 'shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher'
25
- require 'shoulda/matchers/active_model/numericality_matchers/comparison_matcher'
26
27
  require 'shoulda/matchers/active_model/numericality_matchers/odd_number_matcher'
27
28
  require 'shoulda/matchers/active_model/numericality_matchers/even_number_matcher'
28
29
  require 'shoulda/matchers/active_model/numericality_matchers/only_integer_matcher'
@@ -227,14 +227,6 @@ module Shoulda
227
227
  self
228
228
  end
229
229
 
230
- def with(expected_enum_values)
231
- Shoulda::Matchers.warn_about_deprecated_method(
232
- 'The `with` qualifier on `define_enum_for`',
233
- '`with_values`',
234
- )
235
- with_values(expected_enum_values)
236
- end
237
-
238
230
  def with_prefix(expected_prefix = true)
239
231
  options[:prefix] = expected_prefix
240
232
  self
@@ -48,6 +48,30 @@ module Shoulda
48
48
  # should have_db_column(:camera_aperture).of_type(:decimal)
49
49
  # end
50
50
  #
51
+ # ##### of_sql_type
52
+ #
53
+ # Use `of_sql_type` to assert that a column is defined as a certain sql_type.
54
+ #
55
+ # class CreatePhones < ActiveRecord::Migration
56
+ # def change
57
+ # create_table :phones do |t|
58
+ # t.string :camera_aperture, limit: 36
59
+ # end
60
+ # end
61
+ # end
62
+ #
63
+ # # RSpec
64
+ # RSpec.describe Phone, type: :model do
65
+ # it do
66
+ # should have_db_column(:camera_aperture).of_sql_type('varchar(36)')
67
+ # end
68
+ # end
69
+ #
70
+ # # Minitest (Shoulda)
71
+ # class PhoneTest < ActiveSupport::TestCase
72
+ # should have_db_column(:camera_aperture).of_sql_type('varchar(36)')
73
+ # end
74
+ #
51
75
  # ##### with_options
52
76
  #
53
77
  # Use `with_options` to assert that a column has been defined with
@@ -96,6 +120,11 @@ module Shoulda
96
120
  self
97
121
  end
98
122
 
123
+ def of_sql_type(sql_column_type)
124
+ @options[:sql_column_type] = sql_column_type
125
+ self
126
+ end
127
+
99
128
  def with_options(opts = {})
100
129
  validate_options(opts)
101
130
  OPTIONS.each do |attribute|
@@ -110,6 +139,7 @@ module Shoulda
110
139
  @subject = subject
111
140
  column_exists? &&
112
141
  correct_column_type? &&
142
+ correct_sql_column_type? &&
113
143
  correct_precision? &&
114
144
  correct_limit? &&
115
145
  correct_default? &&
@@ -129,12 +159,9 @@ module Shoulda
129
159
 
130
160
  def description
131
161
  desc = "have db column named #{@column}"
132
- if @options.key?(:column_type)
133
- desc << " of type #{@options[:column_type]}"
134
- end
135
- if @options.key?(:precision)
136
- desc << " of precision #{@options[:precision]}"
137
- end
162
+ desc << " of type #{@options[:column_type]}" if @options.key?(:column_type)
163
+ desc << " of sql_type #{@options[:sql_column_type]}" if @options.key?(:sql_column_type)
164
+ desc << " of precision #{@options[:precision]}" if @options.key?(:precision)
138
165
  desc << " of limit #{@options[:limit]}" if @options.key?(:limit)
139
166
  desc << " of default #{@options[:default]}" if @options.key?(:default)
140
167
  desc << " of null #{@options[:null]}" if @options.key?(:null)
@@ -178,6 +205,19 @@ module Shoulda
178
205
  end
179
206
  end
180
207
 
208
+ def correct_sql_column_type?
209
+ return true unless @options.key?(:sql_column_type)
210
+
211
+ if matched_column.sql_type.to_s == @options[:sql_column_type].to_s
212
+ true
213
+ else
214
+ @missing =
215
+ "#{model_class} has a db column named #{@column} " <<
216
+ "of sql type #{matched_column.sql_type}, not #{@options[:sql_column_type]}."
217
+ false
218
+ end
219
+ end
220
+
181
221
  def correct_precision?
182
222
  return true unless @options.key?(:precision)
183
223
 
@@ -197,17 +197,28 @@ module Shoulda
197
197
  def matched_index
198
198
  @_matched_index ||=
199
199
  if expected_columns.one?
200
- actual_indexes.detect do |index|
200
+ sorted_indexes.detect do |index|
201
201
  Array.wrap(index.columns) == expected_columns
202
202
  end
203
203
  else
204
- actual_indexes.detect do |index|
204
+ sorted_indexes.detect do |index|
205
205
  index.columns == expected_columns
206
206
  end
207
207
  end
208
208
  end
209
209
 
210
- def actual_indexes
210
+ def sorted_indexes
211
+ if qualifiers.include?(:unique)
212
+ # return indexes with unique matching the qualifier first
213
+ unsorted_indexes.sort_by do |index|
214
+ index.unique == qualifiers[:unique] ? 0 : 1
215
+ end
216
+ else
217
+ unsorted_indexes
218
+ end
219
+ end
220
+
221
+ def unsorted_indexes
211
222
  model.connection.indexes(table_name)
212
223
  end
213
224
 
@@ -2,7 +2,7 @@ module Shoulda
2
2
  module Matchers
3
3
  module ActiveRecord
4
4
  # The `have_implicit_order_column` matcher tests that the model has `implicit_order_column`
5
- # assigned to one of the table columns. (Rails 6+ only)
5
+ # assigned to one of the table columns.
6
6
  #
7
7
  # class Product < ApplicationRecord
8
8
  # self.implicit_order_column = :created_at
@@ -20,10 +20,8 @@ module Shoulda
20
20
  #
21
21
  # @return [HaveImplicitOrderColumnMatcher]
22
22
  #
23
- if RailsShim.active_record_gte_6?
24
- def have_implicit_order_column(column_name)
25
- HaveImplicitOrderColumnMatcher.new(column_name)
26
- end
23
+ def have_implicit_order_column(column_name)
24
+ HaveImplicitOrderColumnMatcher.new(column_name)
27
25
  end
28
26
 
29
27
  # @private