shoulda-matchers 7.0.1 → 8.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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -4
  3. data/lib/shoulda/matchers/action_controller/render_with_layout_matcher.rb +1 -5
  4. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +1 -1
  5. data/lib/shoulda/matchers/active_model/helpers.rb +3 -1
  6. data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +25 -2
  7. data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +25 -2
  8. data/lib/shoulda/matchers/active_model/validate_comparison_of_matcher.rb +46 -11
  9. data/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +25 -2
  10. data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +25 -2
  11. data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +47 -18
  12. data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +26 -3
  13. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +46 -11
  14. data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +43 -17
  15. data/lib/shoulda/matchers/active_model/validation_matcher.rb +4 -8
  16. data/lib/shoulda/matchers/active_model/validator.rb +4 -0
  17. data/lib/shoulda/matchers/active_record/association_matcher.rb +119 -2
  18. data/lib/shoulda/matchers/active_record/association_matchers/optional_matcher.rb +1 -3
  19. data/lib/shoulda/matchers/active_record/association_matchers/required_matcher.rb +1 -3
  20. data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +7 -1
  21. data/lib/shoulda/matchers/active_record/have_attached_matcher.rb +27 -2
  22. data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +26 -2
  23. data/lib/shoulda/matchers/active_record/uniqueness/namespace.rb +19 -2
  24. data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +4 -14
  25. data/lib/shoulda/matchers/matcher_collection.rb +99 -0
  26. data/lib/shoulda/matchers/rails_shim.rb +4 -4
  27. data/lib/shoulda/matchers/version.rb +1 -1
  28. data/lib/shoulda/matchers.rb +1 -0
  29. data/shoulda-matchers.gemspec +4 -3
  30. metadata +8 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63256c77182faa2d6d70c24f7f53d5cbb4c8f723ebea8ab062a912cea898d405
4
- data.tar.gz: 1bc874eac2eb72cf6e73ff4a8f642deca0f911bb1a8939b7e6d4f257fc1fb3e6
3
+ metadata.gz: 223ff92a4826a7403e0f1016553453323108899c2d2f7029873ea9f99b68a1c0
4
+ data.tar.gz: 2014f3ded4620ce71736c035b8f73959a2a85c1adead8067692173fbca6907de
5
5
  SHA512:
6
- metadata.gz: 943c4db0909a15216ec5aa90cb01a04400cf000217f59dc70bfbfb9a4ac264466f757963ed66c79e1acc9174b4d42f445d9fac8f590d926a6699592f05108633
7
- data.tar.gz: 914455300f3ae6ceccfec6ce50cac85f201643e4b1bca4cd152a2c8a6b2c45e161d7ba8b9fb9daf41ce90c9faeaee75bef5f03b985a3d38768cba5a8a040adf9
6
+ metadata.gz: 3a23155ac210adef58ea71d3cead443b1277b0292972f1365c140b20c671de2f743c39516871ab0ce6ffe0e48b2298a86635533f9fb1f2838577320505b6f91f
7
+ data.tar.gz: 6999714a118645d1aff05c32ad2a7337111aa7c7501f42822a34ca28e31b5d7caa84d49138ecebf0c1dfad17e9f24fd25583e41b73f9e981c4b9ff1da673fd39
data/README.md CHANGED
@@ -63,7 +63,7 @@ Start by including `shoulda-matchers` in your Gemfile:
63
63
 
64
64
  ```ruby
65
65
  group :test do
66
- gem 'shoulda-matchers', '~> 6.0'
66
+ gem 'shoulda-matchers', '~> 8.0'
67
67
  end
68
68
  ```
69
69
 
@@ -125,7 +125,7 @@ Otherwise, add `shoulda-matchers` to your Gemfile:
125
125
 
126
126
  ```ruby
127
127
  group :test do
128
- gem 'shoulda-matchers', '~> 6.0'
128
+ gem 'shoulda-matchers', '~> 8.0'
129
129
  end
130
130
  ```
131
131
 
@@ -341,6 +341,7 @@ RSpec.describe MySpecialModel, type: :model do
341
341
  end
342
342
  ```
343
343
 
344
+ <a name="should-vs-is_expectedto"></a>
344
345
  ### `should` vs `is_expected.to`
345
346
 
346
347
  In this README and throughout the documentation, you'll notice that we use the
@@ -503,14 +504,18 @@ machine, understanding the codebase, and creating a good pull request.
503
504
 
504
505
  ## Compatibility
505
506
 
506
- Shoulda Matchers is tested and supported against Ruby 3.0+, Rails
507
- 6.1+, RSpec 3.x, and Minitest 5.x.
507
+ Shoulda Matchers is tested and supported against Ruby 3.3+, Rails
508
+ 7.2+, RSpec 3.x, and Minitest 5.x.
508
509
 
509
510
  - For Ruby < 2.4 and Rails < 4.1 compatibility, please use [v3.1.3][v3.1.3].
510
511
  - For Ruby < 3.0 and Rails < 6.1 compatibility, please use [v4.5.1][v4.5.1].
512
+ - For Rails < 7.1 compatibility, please use [v6.5.0][v6.5.0].
513
+ - For Ruby < 3.3 and Rails < 7.2 compatibility, please use [v7.0.1][v7.0.1].
511
514
 
512
515
  [v3.1.3]: https://github.com/thoughtbot/shoulda-matchers/tree/v3.1.3
513
516
  [v4.5.1]: https://github.com/thoughtbot/shoulda-matchers/tree/v4.5.1
517
+ [v6.5.0]: https://github.com/thoughtbot/shoulda-matchers/tree/v6.5.0
518
+ [v7.0.1]: https://github.com/thoughtbot/shoulda-matchers/tree/v7.0.1
514
519
 
515
520
  ## Versioning
516
521
 
@@ -111,11 +111,7 @@ module Shoulda
111
111
  end
112
112
 
113
113
  def rendered_with_expected_layout?
114
- if @expected_layout.nil?
115
- true
116
- else
117
- rendered_layouts.include?(@expected_layout)
118
- end
114
+ @expected_layout.nil? || rendered_layouts.include?(@expected_layout)
119
115
  end
120
116
 
121
117
  def rendered_layouts
@@ -458,7 +458,7 @@ module Shoulda
458
458
  message << '.'
459
459
  else
460
460
  message << " producing these validation errors:\n\n"
461
- message << validator.all_formatted_validation_error_messages
461
+ message << validator.formatted_validation_error_messages
462
462
  end
463
463
  end
464
464
 
@@ -7,8 +7,10 @@ module Shoulda
7
7
  format_validation_errors(object.errors)
8
8
  end
9
9
 
10
- def format_validation_errors(errors)
10
+ def format_validation_errors(errors, attr = nil)
11
11
  list_items = errors.to_hash.keys.map do |attribute|
12
+ next if attr && attr.to_sym != attribute.to_sym
13
+
12
14
  messages = errors[attribute]
13
15
  "* #{attribute}: #{messages}"
14
16
  end
@@ -70,10 +70,33 @@ module Shoulda
70
70
  # with_message("there shall be peace on Earth")
71
71
  # end
72
72
  #
73
+ # #### Multiple attributes
74
+ #
75
+ # You can pass multiple attributes to assert that each one has the
76
+ # validation. Any qualifier chained on the matcher is applied to
77
+ # every attribute uniformly.
78
+ #
79
+ # class Robot
80
+ # include ActiveModel::Model
81
+ # attr_accessor :arms, :legs
82
+ #
83
+ # validates_absence_of :arms, :legs
84
+ # end
85
+ #
86
+ # # RSpec
87
+ # RSpec.describe Robot, type: :model do
88
+ # it { should validate_absence_of(:arms, :legs) }
89
+ # end
90
+ #
91
+ # # Minitest (Shoulda)
92
+ # class RobotTest < ActiveSupport::TestCase
93
+ # should validate_absence_of(:arms, :legs)
94
+ # end
95
+ #
73
96
  # @return [ValidateAbsenceOfMatcher}
74
97
  #
75
- def validate_absence_of(attr)
76
- ValidateAbsenceOfMatcher.new(attr)
98
+ def validate_absence_of(*attrs)
99
+ MatcherCollection.build(attrs) { |attr| ValidateAbsenceOfMatcher.new(attr) }
77
100
  end
78
101
 
79
102
  # @private
@@ -73,10 +73,33 @@ module Shoulda
73
73
  # with_message('You must accept the terms of service')
74
74
  # end
75
75
  #
76
+ # #### Multiple attributes
77
+ #
78
+ # You can pass multiple attributes to assert that each one has the
79
+ # validation. Any qualifier chained on the matcher is applied to
80
+ # every attribute uniformly.
81
+ #
82
+ # class User
83
+ # include ActiveModel::Model
84
+ # attr_accessor :terms, :privacy_policy
85
+ #
86
+ # validates_acceptance_of :terms, :privacy_policy
87
+ # end
88
+ #
89
+ # # RSpec
90
+ # RSpec.describe User, type: :model do
91
+ # it { should validate_acceptance_of(:terms, :privacy_policy) }
92
+ # end
93
+ #
94
+ # # Minitest (Shoulda)
95
+ # class UserTest < ActiveSupport::TestCase
96
+ # should validate_acceptance_of(:terms, :privacy_policy)
97
+ # end
98
+ #
76
99
  # @return [ValidateAcceptanceOfMatcher]
77
100
  #
78
- def validate_acceptance_of(attr)
79
- ValidateAcceptanceOfMatcher.new(attr)
101
+ def validate_acceptance_of(*attrs)
102
+ MatcherCollection.build(attrs) { |attr| ValidateAcceptanceOfMatcher.new(attr) }
80
103
  end
81
104
 
82
105
  # @private
@@ -272,10 +272,33 @@ module Shoulda
272
272
  # should validate_comparison_of(:age).is_greater_than(0).allow_nil
273
273
  # end
274
274
  #
275
+ # #### Multiple attributes
276
+ #
277
+ # You can pass multiple attributes to assert that each one has the
278
+ # validation. Any qualifier chained on the matcher is applied to
279
+ # every attribute uniformly.
280
+ #
281
+ # class Item
282
+ # include ActiveModel::Model
283
+ # attr_accessor :width, :height
284
+ #
285
+ # validates_comparison_of :width, :height, greater_than: 0
286
+ # end
287
+ #
288
+ # # RSpec
289
+ # RSpec.describe Item, type: :model do
290
+ # it { should validate_comparison_of(:width, :height).is_greater_than(0) }
291
+ # end
292
+ #
293
+ # # Minitest (Shoulda)
294
+ # class ItemTest < ActiveSupport::TestCase
295
+ # should validate_comparison_of(:width, :height).is_greater_than(0)
296
+ # end
297
+ #
275
298
  # @return [ValidateComparisonOfMatcher]
276
299
  #
277
- def validate_comparison_of(attr)
278
- ValidateComparisonOfMatcher.new(attr)
300
+ def validate_comparison_of(*attrs)
301
+ MatcherCollection.build(attrs) { |attr| ValidateComparisonOfMatcher.new(attr) }
279
302
  end
280
303
 
281
304
  # @private
@@ -378,6 +401,13 @@ module Shoulda
378
401
  end
379
402
  end
380
403
 
404
+ def failure_reason
405
+ raw_submatcher_failure_reason_for(
406
+ first_submatcher_that_fails_to_match,
407
+ :failure_message,
408
+ )
409
+ end
410
+
381
411
  def given_numeric_column?
382
412
  attribute_is_active_record_column? &&
383
413
  [:integer, :float, :decimal].include?(column_type)
@@ -485,21 +515,26 @@ module Shoulda
485
515
  submatcher,
486
516
  failure_message_method
487
517
  )
518
+ Shoulda::Matchers.word_wrap(
519
+ raw_submatcher_failure_reason_for(submatcher, failure_message_method),
520
+ indent: 2,
521
+ )
522
+ end
523
+
524
+ def raw_submatcher_failure_reason_for(submatcher, failure_message_method)
488
525
  failure_message = submatcher.public_send(failure_message_method)
489
526
  submatcher_description = submatcher.simple_description.
490
527
  sub(/\bvalidate that\b/, 'validates').
491
528
  sub(/\bdisallow\b/, 'disallows').
492
529
  sub(/\ballow\b/, 'allows')
493
- submatcher_message =
494
- if number_of_submatchers_for_failure_message > 1
495
- "In checking that #{model.name} #{submatcher_description}, " +
496
- failure_message[0].downcase +
497
- failure_message[1..]
498
- else
499
- failure_message
500
- end
501
530
 
502
- Shoulda::Matchers.word_wrap(submatcher_message, indent: 2)
531
+ if number_of_submatchers_for_failure_message > 1
532
+ "In checking that #{model.name} #{submatcher_description}, " +
533
+ failure_message[0].downcase +
534
+ failure_message[1..]
535
+ else
536
+ failure_message
537
+ end
503
538
  end
504
539
 
505
540
  def comparison_descriptions
@@ -70,10 +70,33 @@ module Shoulda
70
70
  # with_message('Please re-enter your password')
71
71
  # end
72
72
  #
73
+ # #### Multiple attributes
74
+ #
75
+ # You can pass multiple attributes to assert that each one has the
76
+ # validation. Any qualifier chained on the matcher is applied to
77
+ # every attribute uniformly.
78
+ #
79
+ # class User
80
+ # include ActiveModel::Model
81
+ # attr_accessor :password, :password_confirmation, :email, :email_confirmation
82
+ #
83
+ # validates_confirmation_of :password, :email
84
+ # end
85
+ #
86
+ # # RSpec
87
+ # RSpec.describe User, type: :model do
88
+ # it { should validate_confirmation_of(:password, :email) }
89
+ # end
90
+ #
91
+ # # Minitest (Shoulda)
92
+ # class UserTest < ActiveSupport::TestCase
93
+ # should validate_confirmation_of(:password, :email)
94
+ # end
95
+ #
73
96
  # @return [ValidateConfirmationOfMatcher]
74
97
  #
75
- def validate_confirmation_of(attr)
76
- ValidateConfirmationOfMatcher.new(attr)
98
+ def validate_confirmation_of(*attrs)
99
+ MatcherCollection.build(attrs) { |attr| ValidateConfirmationOfMatcher.new(attr) }
77
100
  end
78
101
 
79
102
  # @private
@@ -112,10 +112,33 @@ module Shoulda
112
112
  # with_message('You chose a puny weapon')
113
113
  # end
114
114
  #
115
+ # #### Multiple attributes
116
+ #
117
+ # You can pass multiple attributes to assert that each one has the
118
+ # validation. Any qualifier chained on the matcher is applied to
119
+ # every attribute uniformly.
120
+ #
121
+ # class Article
122
+ # include ActiveModel::Model
123
+ # attr_accessor :slug, :handle
124
+ #
125
+ # validates_exclusion_of :slug, :handle, in: %w[admin root]
126
+ # end
127
+ #
128
+ # # RSpec
129
+ # RSpec.describe Article, type: :model do
130
+ # it { should validate_exclusion_of(:slug, :handle).in_array(%w[admin root]) }
131
+ # end
132
+ #
133
+ # # Minitest (Shoulda)
134
+ # class ArticleTest < ActiveSupport::TestCase
135
+ # should validate_exclusion_of(:slug, :handle).in_array(%w[admin root])
136
+ # end
137
+ #
115
138
  # @return [ValidateExclusionOfMatcher]
116
139
  #
117
- def validate_exclusion_of(attr)
118
- ValidateExclusionOfMatcher.new(attr)
140
+ def validate_exclusion_of(*attrs)
141
+ MatcherCollection.build(attrs) { |attr| ValidateExclusionOfMatcher.new(attr) }
119
142
  end
120
143
 
121
144
  # @private
@@ -263,10 +263,33 @@ module Shoulda
263
263
  # allow_blank
264
264
  # end
265
265
  #
266
+ # #### Multiple attributes
267
+ #
268
+ # You can pass multiple attributes to assert that each one has the
269
+ # validation. Any qualifier chained on the matcher is applied to
270
+ # every attribute uniformly.
271
+ #
272
+ # class Article
273
+ # include ActiveModel::Model
274
+ # attr_accessor :status, :category
275
+ #
276
+ # validates_inclusion_of :status, :category, in: %w[draft published]
277
+ # end
278
+ #
279
+ # # RSpec
280
+ # RSpec.describe Article, type: :model do
281
+ # it { should validate_inclusion_of(:status, :category).in_array(%w[draft published]) }
282
+ # end
283
+ #
284
+ # # Minitest (Shoulda)
285
+ # class ArticleTest < ActiveSupport::TestCase
286
+ # should validate_inclusion_of(:status, :category).in_array(%w[draft published])
287
+ # end
288
+ #
266
289
  # @return [ValidateInclusionOfMatcher]
267
290
  #
268
- def validate_inclusion_of(attr)
269
- ValidateInclusionOfMatcher.new(attr)
291
+ def validate_inclusion_of(*attrs)
292
+ MatcherCollection.build(attrs) { |attr| ValidateInclusionOfMatcher.new(attr) }
270
293
  end
271
294
 
272
295
  # @private
@@ -278,18 +301,6 @@ module Shoulda
278
301
  ARBITRARY_OUTSIDE_DATE = Date.jd(9999999)
279
302
  ARBITRARY_OUTSIDE_DATETIME = DateTime.jd(9999999)
280
303
  ARBITRARY_OUTSIDE_TIME = Time.at(9999999999)
281
- BOOLEAN_ALLOWS_BOOLEAN_MESSAGE = <<EOT.freeze
282
- You are using `validate_inclusion_of` to assert that a boolean column allows
283
- boolean values and disallows non-boolean ones. Be aware that it is not possible
284
- to fully test this, as boolean columns will automatically convert non-boolean
285
- values to boolean ones. Hence, you should consider removing this test.
286
- EOT
287
- BOOLEAN_ALLOWS_NIL_MESSAGE = <<EOT.freeze
288
- You are using `validate_inclusion_of` to assert that a boolean column allows nil.
289
- Be aware that it is not possible to fully test this, as anything other than
290
- true, false or nil will be converted to false. Hence, you should consider
291
- removing this test.
292
- EOT
293
304
 
294
305
  def initialize(attribute)
295
306
  super(attribute)
@@ -400,6 +411,24 @@ EOT
400
411
 
401
412
  private
402
413
 
414
+ def boolean_allows_boolean_message
415
+ <<-EOT.strip
416
+ You are using `validate_inclusion_of` to assert that the column '#{@subject.class.name}##{attribute}'
417
+ allows boolean values and disallows non-boolean ones. Be aware that it
418
+ is not possible to fully test this, as boolean columns will automatically
419
+ convert non-boolean values to boolean ones. Hence, you should consider
420
+ removing this test.
421
+ EOT
422
+ end
423
+
424
+ def boolean_allows_nil_message
425
+ <<-EOT.strip
426
+ You are using `validate_inclusion_of` to assert that the column '#{@subject.class.name}##{attribute}'
427
+ allows nil. Be aware that it is not possible to fully test this, as anything other than
428
+ true, false or nil will be converted to false. Hence, you should consider removing this test.
429
+ EOT
430
+ end
431
+
403
432
  def minimum_range_value
404
433
  @range.begin
405
434
  end
@@ -492,11 +521,11 @@ EOT
492
521
  if attribute_type == :boolean
493
522
  case @array
494
523
  when [false, true], [true, false]
495
- Shoulda::Matchers.warn BOOLEAN_ALLOWS_BOOLEAN_MESSAGE
524
+ Shoulda::Matchers.warn boolean_allows_boolean_message
496
525
  return true
497
526
  when [nil]
498
527
  if attribute_column.null
499
- Shoulda::Matchers.warn BOOLEAN_ALLOWS_NIL_MESSAGE
528
+ Shoulda::Matchers.warn boolean_allows_nil_message
500
529
  return true
501
530
  else
502
531
  raise NonNullableBooleanError.create(@attribute)
@@ -513,11 +542,11 @@ EOT
513
542
  if attribute_type == :boolean
514
543
  case @array
515
544
  when [false, true], [true, false]
516
- Shoulda::Matchers.warn BOOLEAN_ALLOWS_BOOLEAN_MESSAGE
545
+ Shoulda::Matchers.warn boolean_allows_boolean_message
517
546
  return true
518
547
  when [nil]
519
548
  if attribute_column.null
520
- Shoulda::Matchers.warn BOOLEAN_ALLOWS_NIL_MESSAGE
549
+ Shoulda::Matchers.warn boolean_allows_nil_message
521
550
  return true
522
551
  else
523
552
  raise NonNullableBooleanError.create(@attribute)
@@ -240,7 +240,6 @@ module Shoulda
240
240
  # should validate_length_of(:bio).is_at_least(15).allow_nil
241
241
  # end
242
242
  #
243
- # @return [ValidateLengthOfMatcher]
244
243
  #
245
244
  # ##### allow_blank
246
245
  #
@@ -286,8 +285,32 @@ module Shoulda
286
285
  # should validate_length_of(:arr).as_array.is_at_least(15)
287
286
  # end
288
287
  #
289
- def validate_length_of(attr)
290
- ValidateLengthOfMatcher.new(attr)
288
+ # #### Multiple attributes
289
+ #
290
+ # You can pass multiple attributes to assert that each one has the
291
+ # validation. Any qualifier chained on the matcher is applied to
292
+ # every attribute uniformly.
293
+ #
294
+ # class User
295
+ # include ActiveModel::Model
296
+ # attr_accessor :first_name, :last_name
297
+ #
298
+ # validates_length_of :first_name, :last_name, minimum: 2
299
+ # end
300
+ #
301
+ # # RSpec
302
+ # RSpec.describe User, type: :model do
303
+ # it { should validate_length_of(:first_name, :last_name).is_at_least(2) }
304
+ # end
305
+ #
306
+ # # Minitest (Shoulda)
307
+ # class UserTest < ActiveSupport::TestCase
308
+ # should validate_length_of(:first_name, :last_name).is_at_least(2)
309
+ # end
310
+ #
311
+ # @return [ValidateLengthOfMatcher]
312
+ def validate_length_of(*attrs)
313
+ MatcherCollection.build(attrs) { |attr| ValidateLengthOfMatcher.new(attr) }
291
314
  end
292
315
 
293
316
  # @private
@@ -350,10 +350,33 @@ module Shoulda
350
350
  # should validate_numericality_of(:age).allow_nil
351
351
  # end
352
352
  #
353
+ # #### Multiple attributes
354
+ #
355
+ # You can pass multiple attributes to assert that each one has the
356
+ # validation. Any qualifier chained on the matcher is applied to
357
+ # every attribute uniformly.
358
+ #
359
+ # class Item
360
+ # include ActiveModel::Model
361
+ # attr_accessor :price, :quantity
362
+ #
363
+ # validates_numericality_of :price, :quantity
364
+ # end
365
+ #
366
+ # # RSpec
367
+ # RSpec.describe Item, type: :model do
368
+ # it { should validate_numericality_of(:price, :quantity) }
369
+ # end
370
+ #
371
+ # # Minitest (Shoulda)
372
+ # class ItemTest < ActiveSupport::TestCase
373
+ # should validate_numericality_of(:price, :quantity)
374
+ # end
375
+ #
353
376
  # @return [ValidateNumericalityOfMatcher]
354
377
  #
355
- def validate_numericality_of(attr)
356
- ValidateNumericalityOfMatcher.new(attr)
378
+ def validate_numericality_of(*attrs)
379
+ MatcherCollection.build(attrs) { |attr| ValidateNumericalityOfMatcher.new(attr) }
357
380
  end
358
381
 
359
382
  # @private
@@ -482,6 +505,13 @@ module Shoulda
482
505
  end
483
506
  end
484
507
 
508
+ def failure_reason
509
+ raw_submatcher_failure_reason_for(
510
+ first_submatcher_that_fails_to_match,
511
+ :failure_message,
512
+ )
513
+ end
514
+
485
515
  def given_numeric_column?
486
516
  attribute_is_active_record_column? &&
487
517
  [:integer, :float, :decimal].include?(column_type)
@@ -620,21 +650,26 @@ module Shoulda
620
650
  submatcher,
621
651
  failure_message_method
622
652
  )
653
+ Shoulda::Matchers.word_wrap(
654
+ raw_submatcher_failure_reason_for(submatcher, failure_message_method),
655
+ indent: 2,
656
+ )
657
+ end
658
+
659
+ def raw_submatcher_failure_reason_for(submatcher, failure_message_method)
623
660
  failure_message = submatcher.public_send(failure_message_method)
624
661
  submatcher_description = submatcher.simple_description.
625
662
  sub(/\bvalidate that\b/, 'validates').
626
663
  sub(/\bdisallow\b/, 'disallows').
627
664
  sub(/\ballow\b/, 'allows')
628
- submatcher_message =
629
- if number_of_submatchers_for_failure_message > 1
630
- "In checking that #{model.name} #{submatcher_description}, " +
631
- failure_message[0].downcase +
632
- failure_message[1..]
633
- else
634
- failure_message
635
- end
636
665
 
637
- Shoulda::Matchers.word_wrap(submatcher_message, indent: 2)
666
+ if number_of_submatchers_for_failure_message > 1
667
+ "In checking that #{model.name} #{submatcher_description}, " +
668
+ failure_message[0].downcase +
669
+ failure_message[1..]
670
+ else
671
+ failure_message
672
+ end
638
673
  end
639
674
 
640
675
  def full_allowed_type
@@ -145,10 +145,34 @@ module Shoulda
145
145
  # with_message('Robot has no legs')
146
146
  # end
147
147
  #
148
+ # #### Multiple attributes
149
+ #
150
+ # You can pass multiple attributes to assert that each one has the
151
+ # validation. Any qualifier chained on the matcher is applied to
152
+ # every attribute uniformly.
153
+ #
154
+ # class Robot
155
+ # include ActiveModel::Model
156
+ # attr_accessor :arms, :legs
157
+ #
158
+ # validates_presence_of :arms, :legs
159
+ # end
160
+ #
161
+ # # RSpec
162
+ # RSpec.describe Robot, type: :model do
163
+ # it { should validate_presence_of(:arms, :legs) }
164
+ # end
165
+ #
166
+ # # Minitest (Shoulda)
167
+ # class RobotTest < ActiveSupport::TestCase
168
+ # should validate_presence_of(:arms, :legs)
169
+ # end
170
+ #
148
171
  # @return [ValidatePresenceOfMatcher]
149
172
  #
150
- def validate_presence_of(attr)
151
- ValidatePresenceOfMatcher.new(attr)
173
+
174
+ def validate_presence_of(*attrs)
175
+ MatcherCollection.build(attrs) { |attr| ValidatePresenceOfMatcher.new(attr) }
152
176
  end
153
177
 
154
178
  # @private
@@ -206,27 +230,29 @@ module Shoulda
206
230
  "validate that :#{@attribute} cannot be empty/falsy"
207
231
  end
208
232
 
209
- def failure_message
210
- message = super
211
-
233
+ def failure_reason
234
+ reason = super
212
235
  if should_add_footnote_about_belongs_to?
213
- message << "\n\n"
214
- message << Shoulda::Matchers.word_wrap(<<-MESSAGE.strip, indent: 2)
215
- You're getting this error because #{reason_for_existing_presence_validation}.
216
- *This* presence validation doesn't use "can't be blank", the usual validation
217
- message, but "must exist" instead.
218
-
219
- With that said, did you know that the `belong_to` matcher can test this
220
- validation for you? Instead of using `validate_presence_of`, try
221
- #{suggestions_for_belongs_to}
222
- MESSAGE
236
+ "#{reason}\n\n#{belongs_to_footnote}"
237
+ else
238
+ reason
223
239
  end
224
-
225
- message
226
240
  end
227
241
 
228
242
  private
229
243
 
244
+ def belongs_to_footnote
245
+ <<~MESSAGE.strip
246
+ You're getting this error because #{reason_for_existing_presence_validation}.
247
+ *This* presence validation doesn't use "can't be blank", the usual validation
248
+ message, but "must exist" instead.
249
+
250
+ With that said, did you know that the `belong_to` matcher can test this
251
+ validation for you? Instead of using `validate_presence_of`, try
252
+ #{suggestions_for_belongs_to}
253
+ MESSAGE
254
+ end
255
+
230
256
  def secure_password_being_validated?
231
257
  Shoulda::Matchers::RailsShim.digestible_attributes_in(@subject).
232
258
  include?(@attribute)
@@ -86,6 +86,10 @@ module Shoulda
86
86
  end
87
87
  end
88
88
 
89
+ def failure_reason
90
+ last_submatcher_run.try(:failure_message)
91
+ end
92
+
89
93
  protected
90
94
 
91
95
  attr_reader :attribute, :context, :subject, :last_submatcher_run
@@ -150,14 +154,6 @@ module Shoulda
150
154
  )
151
155
  end
152
156
 
153
- def failure_reason
154
- last_submatcher_run.try(:failure_message)
155
- end
156
-
157
- def failure_reason_when_negated
158
- last_submatcher_run.try(:failure_message_when_negated)
159
- end
160
-
161
157
  def build_allow_or_disallow_value_matcher(args)
162
158
  matcher_class = args.fetch(:matcher_class)
163
159
  value = args.fetch(:value)
@@ -45,6 +45,10 @@ module Shoulda
45
45
  validation_result[:validation_exception_message]
46
46
  end
47
47
 
48
+ def formatted_validation_error_messages
49
+ format_validation_errors(all_validation_errors, attribute)
50
+ end
51
+
48
52
  protected
49
53
 
50
54
  attr_reader :attribute, :context, :record
@@ -372,6 +372,25 @@ module Shoulda
372
372
  # should belong_to(:organization).optional
373
373
  # end
374
374
  #
375
+ # ##### deprecated
376
+ #
377
+ # Use `deprecated` to assert that the `:deprecated` option was specified.
378
+ # (Enabled by default in Rails 8.1+).
379
+ #
380
+ # class Account < ActiveRecord::Base
381
+ # belongs_to :bank, deprecated: true
382
+ # end
383
+ #
384
+ # # RSpec
385
+ # RSpec.describe Account, type: :model do
386
+ # it { should belong_to(:bank).deprecated(true) }
387
+ # end
388
+ #
389
+ # # Minitest (Shoulda)
390
+ # class AccountTest < ActiveSupport::TestCase
391
+ # should belong_to(:bank).deprecated(true)
392
+ # end
393
+ #
375
394
  # @return [AssociationMatcher]
376
395
  #
377
396
  def belong_to(name)
@@ -681,9 +700,27 @@ module Shoulda
681
700
  # should have_delegated_type(:drivable).optional
682
701
  # end
683
702
  #
703
+ # ##### deprecated
704
+ #
705
+ # Use `deprecated` to assert that the `:deprecated` option was specified.
706
+ # (Enabled by default in Rails 8.1+).
707
+ #
708
+ # class Vehicle < ActiveRecord::Base
709
+ # delegated_type :drivable, types: %w(Car Truck), deprecated: true
710
+ # end
711
+ #
712
+ # # RSpec
713
+ # describe Vehicle
714
+ # it { should have_delegated_type(:drivable).deprecated }
715
+ # end
716
+ #
717
+ # # Minitest (Shoulda)
718
+ # class VehicleTest < ActiveSupport::TestCase
719
+ # should have_delegated_type(:drivable).deprecated
720
+ # end
721
+ #
684
722
  # @return [AssociationMatcher]
685
723
  #
686
-
687
724
  def have_delegated_type(name)
688
725
  AssociationMatcher.new(:belongs_to, name)
689
726
  end
@@ -822,7 +859,7 @@ module Shoulda
822
859
  # Use `with_foreign_type` to test usage of the `:foreign_type` option.
823
860
  #
824
861
  # class Hotel < ActiveRecord::Base
825
- # has_many :visitors, foreign_key: 'facility_type', as: :location
862
+ # has_many :visitors, foreign_type: 'facility_type', as: :location
826
863
  # end
827
864
  #
828
865
  # # RSpec
@@ -970,6 +1007,26 @@ module Shoulda
970
1007
  # should have_many(:employees).inverse_of(:company)
971
1008
  # end
972
1009
  #
1010
+ #
1011
+ # ##### deprecated
1012
+ #
1013
+ # Use `deprecated` to assert that the `:deprecated` option was specified.
1014
+ # (Enabled by default in Rails 8.1+)
1015
+ #
1016
+ # class Player < ActiveRecord::Base
1017
+ # has_many :games, deprecated: true
1018
+ # end
1019
+ #
1020
+ # # RSpec
1021
+ # RSpec.describe Player, type: :model do
1022
+ # it { should have_many(:games).deprecated(true) }
1023
+ # end
1024
+ #
1025
+ # # Minitest (Shoulda)
1026
+ # class PlayerTest < ActiveSupport::TestCase
1027
+ # should have_many(:games).deprecated(true)
1028
+ # end
1029
+ #
973
1030
  # @return [AssociationMatcher]
974
1031
  #
975
1032
  def have_many(name)
@@ -1217,6 +1274,26 @@ module Shoulda
1217
1274
  # should have_one(:brain).required
1218
1275
  # end
1219
1276
  #
1277
+ #
1278
+ # ##### deprecated
1279
+ #
1280
+ # Use `deprecated` to assert that the `:deprecated` option was specified.
1281
+ # (Enabled by default in Rails 8.1+).
1282
+ #
1283
+ # class Account < ActiveRecord::Base
1284
+ # has_one :bank, deprecated: true
1285
+ # end
1286
+ #
1287
+ # # RSpec
1288
+ # RSpec.describe Account, type: :model do
1289
+ # it { should have_one(:bank).deprecated(true) }
1290
+ # end
1291
+ #
1292
+ # # Minitest (Shoulda)
1293
+ # class AccountTest < ActiveSupport::TestCase
1294
+ # should have_one(:bank).deprecated(true)
1295
+ # end
1296
+ #
1220
1297
  # @return [AssociationMatcher]
1221
1298
  #
1222
1299
  def have_one(name)
@@ -1375,6 +1452,25 @@ module Shoulda
1375
1452
  # should have_and_belong_to_many(:advertisers).autosave(true)
1376
1453
  # end
1377
1454
  #
1455
+ # ##### deprecated
1456
+ #
1457
+ # Use `deprecated` to assert that the `:deprecated` option was specified.
1458
+ # (Enabled by default in Rails 8.1+).
1459
+ #
1460
+ # class Publisher < ActiveRecord::Base
1461
+ # has_and_belongs_to_many :advertisers, deprecated: true
1462
+ # end
1463
+ #
1464
+ # # RSpec
1465
+ # RSpec.describe Publisher, type: :model do
1466
+ # it { should have_and_belong_to_many(:advertisers).deprecated(true) }
1467
+ # end
1468
+ #
1469
+ # # Minitest (Shoulda)
1470
+ # class AccountTest < ActiveSupport::TestCase
1471
+ # should have_and_belong_to_many(:advertisers).deprecated(true)
1472
+ # end
1473
+ #
1378
1474
  # @return [AssociationMatcher]
1379
1475
  #
1380
1476
  def have_and_belong_to_many(name)
@@ -1546,6 +1642,16 @@ module Shoulda
1546
1642
  self
1547
1643
  end
1548
1644
 
1645
+ def deprecated(deprecated = true)
1646
+ if Shoulda::Matchers::RailsShim.active_record_gte_8_1?
1647
+ @options[:deprecated] = deprecated
1648
+ self
1649
+ else
1650
+ raise NotImplementedError,
1651
+ '`deprecated` association matcher is only available on Active Record >= 8.1.'
1652
+ end
1653
+ end
1654
+
1549
1655
  def without_validating_presence
1550
1656
  remove_submatcher(AssociationMatchers::RequiredMatcher)
1551
1657
  self
@@ -1586,6 +1692,7 @@ module Shoulda
1586
1692
  touch_correct? &&
1587
1693
  types_correct? &&
1588
1694
  strict_loading_correct? &&
1695
+ deprecated_correct? &&
1589
1696
  submatchers_match?
1590
1697
  end
1591
1698
 
@@ -1848,6 +1955,16 @@ module Shoulda
1848
1955
  end
1849
1956
  end
1850
1957
 
1958
+ def deprecated_correct?
1959
+ if option_verifier.correct_for_boolean?(:deprecated, options[:deprecated])
1960
+ true
1961
+ else
1962
+ @missing = "#{name} should have deprecated set to"\
1963
+ " #{options[:deprecated]}"
1964
+ false
1965
+ end
1966
+ end
1967
+
1851
1968
  def types_correct?
1852
1969
  if options.key?(:types)
1853
1970
  types = options[:types]
@@ -49,12 +49,10 @@ module Shoulda
49
49
  'to '
50
50
  end
51
51
 
52
- missing_option_string << (
53
- 'fail validation if '\
52
+ missing_option_string << 'fail validation if '\
54
53
  ":#{attribute_name} is unset; i.e., either the association "\
55
54
  'should have been defined with `optional: '\
56
55
  "#{optional.inspect}`, or there "
57
- )
58
56
 
59
57
  missing_option_string <<
60
58
  if optional
@@ -54,12 +54,10 @@ module Shoulda
54
54
  'not to '
55
55
  end
56
56
 
57
- missing_option_string << (
58
- 'fail validation if '\
57
+ missing_option_string << 'fail validation if '\
59
58
  ":#{attribute_name} is unset; i.e., either the association "\
60
59
  'should have been defined with `required: '\
61
60
  "#{required.inspect}`, or there "
62
- )
63
61
 
64
62
  missing_option_string <<
65
63
  if required
@@ -320,6 +320,12 @@ module Shoulda
320
320
  self
321
321
  end
322
322
 
323
+ def with_options(**)
324
+ raise NotImplementedError,
325
+ 'with_options is not a valid qualifier for the define_enum_for matcher. '\
326
+ 'Did you mean to use with_values instead?'
327
+ end
328
+
323
329
  def with_prefix(expected_prefix = true)
324
330
  options[:prefix] = expected_prefix
325
331
  self
@@ -667,7 +673,7 @@ module Shoulda
667
673
  end
668
674
 
669
675
  value = case attribute_schema
670
- in [_, { default: default_value } ]
676
+ in [_, { default: default_value }]
671
677
  default_value
672
678
  in [_, default_value, *]
673
679
  default_value
@@ -88,10 +88,31 @@ module Shoulda
88
88
  # should have_one_attached(:avatar).strict_loading
89
89
  # end
90
90
  #
91
+ # #### Multiple attributes
92
+ #
93
+ # You can pass multiple attributes to assert that each one has the
94
+ # validation. Any qualifier chained on the matcher is applied to
95
+ # every attribute uniformly.
96
+ #
97
+ # class User < ActiveRecord::Base
98
+ # has_one_attached :avatar
99
+ # has_one_attached :cover
100
+ # end
101
+ #
102
+ # # RSpec
103
+ # RSpec.describe User, type: :model do
104
+ # it { should have_one_attached(:avatar, :cover) }
105
+ # end
106
+ #
107
+ # # Minitest (Shoulda)
108
+ # class UserTest < ActiveSupport::TestCase
109
+ # should have_one_attached(:avatar, :cover)
110
+ # end
111
+ #
91
112
  # @return [HaveAttachedMatcher]
92
113
  #
93
- def have_one_attached(name)
94
- HaveAttachedMatcher.new(:one, name)
114
+ def have_one_attached(*names)
115
+ MatcherCollection.build(names) { |name| HaveAttachedMatcher.new(:one, name) }
95
116
  end
96
117
 
97
118
  # The `have_many_attached` matcher tests usage of the
@@ -140,6 +161,10 @@ Expected #{expectation}, but this could not be proved.
140
161
  MESSAGE
141
162
  end
142
163
 
164
+ def failure_reason
165
+ @failure
166
+ end
167
+
143
168
  def failure_message_when_negated
144
169
  <<-MESSAGE
145
170
  Did not expect #{expectation}, but it does.
@@ -18,10 +18,30 @@ module Shoulda
18
18
  # should have_readonly_attribute(:password)
19
19
  # end
20
20
  #
21
+ # #### Multiple attributes
22
+ #
23
+ # You can pass multiple attributes to assert that each one has the
24
+ # validation. Any qualifier chained on the matcher is applied to
25
+ # every attribute uniformly.
26
+ #
27
+ # class User < ActiveRecord::Base
28
+ # attr_readonly :name, :email
29
+ # end
30
+ #
31
+ # # RSpec
32
+ # RSpec.describe User, type: :model do
33
+ # it { should have_readonly_attribute(:name, :email) }
34
+ # end
35
+ #
36
+ # # Minitest (Shoulda)
37
+ # class UserTest < ActiveSupport::TestCase
38
+ # should have_readonly_attribute(:name, :email)
39
+ # end
40
+ #
21
41
  # @return [HaveReadonlyAttributeMatcher]
22
42
  #
23
- def have_readonly_attribute(value)
24
- HaveReadonlyAttributeMatcher.new(value)
43
+ def have_readonly_attribute(*values)
44
+ MatcherCollection.build(values) { |value| HaveReadonlyAttributeMatcher.new(value) }
25
45
  end
26
46
 
27
47
  # @private
@@ -32,6 +52,10 @@ module Shoulda
32
52
 
33
53
  attr_reader :failure_message, :failure_message_when_negated
34
54
 
55
+ def failure_reason
56
+ @failure_message
57
+ end
58
+
35
59
  def matches?(subject)
36
60
  @subject = subject
37
61
  if readonly_attributes.include?(@attribute)
@@ -10,7 +10,7 @@ module Shoulda
10
10
  end
11
11
 
12
12
  def has?(name)
13
- constant.const_defined?(name)
13
+ constant.const_defined?(name, false)
14
14
  end
15
15
 
16
16
  def set(name, value)
@@ -18,8 +18,25 @@ module Shoulda
18
18
  end
19
19
 
20
20
  def clear
21
+ test_model_classes = []
22
+
21
23
  constant.constants.each do |child_constant|
22
- constant.__send__(:remove_const, child_constant)
24
+ child = constant.const_get(child_constant)
25
+
26
+ if defined?(::ActiveRecord::Base) &&
27
+ child.is_a?(Class) &&
28
+ child < ::ActiveRecord::Base
29
+ test_model_classes << child
30
+ end
31
+
32
+ constant.remove_const(child_constant)
33
+ rescue NameError
34
+ # Constant may have been removed elsewhere; ignore
35
+ end
36
+
37
+ if defined?(::ActiveSupport::DescendantsTracker) &&
38
+ test_model_classes.any?
39
+ ::ActiveSupport::DescendantsTracker.clear(test_model_classes)
23
40
  end
24
41
  end
25
42
 
@@ -304,7 +304,6 @@ module Shoulda
304
304
  }
305
305
  @existing_record_created = false
306
306
  @failure_reason = nil
307
- @failure_reason_when_negated = nil
308
307
  @attribute_setters = {
309
308
  existing_record: AttributeSetters.new,
310
309
  new_record: AttributeSetters.new,
@@ -407,10 +406,6 @@ module Shoulda
407
406
  @failure_reason || super
408
407
  end
409
408
 
410
- def failure_reason_when_negated
411
- @failure_reason_when_negated || super
412
- end
413
-
414
409
  def build_allow_or_disallow_value_matcher(args)
415
410
  super.tap do |matcher|
416
411
  matcher.failure_message_preface = method(:failure_message_preface)
@@ -558,12 +553,9 @@ module Shoulda
558
553
  end
559
554
 
560
555
  def does_not_match_allow_nil?
561
- expects_to_allow_nil? && (
562
- update_existing_record!(nil) &&
563
- (@failure_reason = nil ||
564
- disallows_value_of(nil, @expected_message)
556
+ expects_to_allow_nil? && update_existing_record!(nil) &&
557
+ (@failure_reason = disallows_value_of(nil, @expected_message)
565
558
  )
566
- )
567
559
  end
568
560
 
569
561
  def matches_allow_blank?
@@ -574,10 +566,8 @@ module Shoulda
574
566
  end
575
567
 
576
568
  def does_not_match_allow_blank?
577
- expects_to_allow_blank? && (
578
- update_existing_record!('') &&
579
- (@failure_reason = nil || disallows_value_of('', @expected_message))
580
- )
569
+ expects_to_allow_blank? && update_existing_record!('') &&
570
+ (@failure_reason = disallows_value_of('', @expected_message))
581
571
  end
582
572
 
583
573
  def existing_record
@@ -0,0 +1,99 @@
1
+ module Shoulda
2
+ module Matchers
3
+ # @private
4
+ class MatcherCollection
5
+ def self.build(attrs, &block)
6
+ new(attrs.map(&block))
7
+ end
8
+
9
+ def initialize(matchers)
10
+ @matchers = matchers
11
+ @failed_matchers = []
12
+ end
13
+
14
+ def description
15
+ matchers.map(&:description).join(' and ')
16
+ end
17
+
18
+ def matches?(subject)
19
+ @subject = subject
20
+ @failed_matchers = matchers.reject do |matcher|
21
+ matcher.matches?(fresh_subject_for(subject))
22
+ end
23
+ @failed_matchers.empty?
24
+ end
25
+
26
+ def does_not_match?(subject)
27
+ @subject = subject
28
+ @failed_matchers = matchers.reject do |matcher|
29
+ fresh_subject = fresh_subject_for(subject)
30
+ if matcher.respond_to?(:does_not_match?)
31
+ matcher.does_not_match?(fresh_subject)
32
+ else
33
+ !matcher.matches?(fresh_subject)
34
+ end
35
+ end
36
+ @failed_matchers.empty?
37
+ end
38
+
39
+ def failure_message
40
+ if matchers.one?
41
+ matchers.first.failure_message
42
+ else
43
+ build_failure_message('to')
44
+ end
45
+ end
46
+
47
+ def failure_message_when_negated
48
+ if matchers.one?
49
+ matchers.first.failure_message_when_negated
50
+ else
51
+ build_failure_message('not to')
52
+ end
53
+ end
54
+
55
+ def method_missing(method, *args, &block)
56
+ if all_matchers_respond_to?(method)
57
+ matchers.each { |matcher| matcher.send(method, *args, &block) }
58
+ self
59
+ else
60
+ super
61
+ end
62
+ end
63
+
64
+ def respond_to_missing?(method, include_private = false)
65
+ all_matchers_respond_to?(method) || super
66
+ end
67
+
68
+ private
69
+
70
+ attr_reader :matchers
71
+
72
+ def fresh_subject_for(subject)
73
+ matchers.one? ? subject : subject.dup
74
+ end
75
+
76
+ def build_failure_message(direction)
77
+ header = Shoulda::Matchers.word_wrap(
78
+ "Expected #{@subject.class.name} #{direction} "\
79
+ "#{failed_description}, but this could not be proved.",
80
+ )
81
+
82
+ reasons = @failed_matchers.filter_map do |matcher|
83
+ reason = matcher.failure_reason
84
+ Shoulda::Matchers.word_wrap(reason, indent: 2) if reason.present?
85
+ end
86
+
87
+ ([header] + reasons).join("\n")
88
+ end
89
+
90
+ def failed_description
91
+ @failed_matchers.map(&:description).join(' and ')
92
+ end
93
+
94
+ def all_matchers_respond_to?(method)
95
+ matchers.all? { |matcher| matcher.respond_to?(method) }
96
+ end
97
+ end
98
+ end
99
+ end
@@ -25,6 +25,10 @@ module Shoulda
25
25
  Gem::Requirement.new('< 7').satisfied_by?(active_model_version)
26
26
  end
27
27
 
28
+ def active_record_gte_8_1?
29
+ Gem::Requirement.new('>= 8.1').satisfied_by?(active_record_version)
30
+ end
31
+
28
32
  def generate_validation_message(
29
33
  record,
30
34
  attribute,
@@ -139,10 +143,6 @@ module Shoulda
139
143
  model.respond_to?(:attribute_types)
140
144
  end
141
145
 
142
- def validates_column_options?
143
- Gem::Requirement.new('>= 7.1.0').satisfied_by?(active_record_version)
144
- end
145
-
146
146
  private
147
147
 
148
148
  def simply_generate_validation_message(
@@ -1,6 +1,6 @@
1
1
  module Shoulda
2
2
  module Matchers
3
3
  # @private
4
- VERSION = '7.0.1'.freeze
4
+ VERSION = '8.0.0'.freeze
5
5
  end
6
6
  end
@@ -3,6 +3,7 @@ require 'shoulda/matchers/doublespeak'
3
3
  require 'shoulda/matchers/error'
4
4
  require 'shoulda/matchers/independent'
5
5
  require 'shoulda/matchers/integrations'
6
+ require 'shoulda/matchers/matcher_collection'
6
7
  require 'shoulda/matchers/matcher_context'
7
8
  require 'shoulda/matchers/rails_shim'
8
9
  require 'shoulda/matchers/util'
@@ -30,13 +30,14 @@ Gem::Specification.new do |s|
30
30
  'documentation_uri' => 'https://matchers.shoulda.io/docs',
31
31
  'homepage_uri' => 'https://matchers.shoulda.io',
32
32
  'source_code_uri' => 'https://github.com/thoughtbot/shoulda-matchers',
33
+ 'rubygems_mfa_required' => 'true',
33
34
  }
34
35
 
35
36
  s.files = Dir['{docs,lib}/**/*', 'README.md', 'LICENSE',
36
- 'shoulda-matchers.gemspec']
37
+ 'shoulda-matchers.gemspec',]
37
38
  s.require_paths = ['lib']
38
39
 
39
- s.required_ruby_version = '>= 3.2'
40
- s.add_dependency 'activesupport', '>= 7.1'
40
+ s.required_ruby_version = '>= 3.3'
41
+ s.add_dependency 'activesupport', '>= 7.2'
41
42
  s.add_development_dependency 'mutex_m'
42
43
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shoulda-matchers
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.1
4
+ version: 8.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tammer Saleh
@@ -13,7 +13,7 @@ authors:
13
13
  - Elliot Winkler
14
14
  bindir: bin
15
15
  cert_chain: []
16
- date: 2025-10-31 00:00:00.000000000 Z
16
+ date: 2026-06-12 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: activesupport
@@ -21,14 +21,14 @@ dependencies:
21
21
  requirements:
22
22
  - - ">="
23
23
  - !ruby/object:Gem::Version
24
- version: '7.1'
24
+ version: '7.2'
25
25
  type: :runtime
26
26
  prerelease: false
27
27
  version_requirements: !ruby/object:Gem::Requirement
28
28
  requirements:
29
29
  - - ">="
30
30
  - !ruby/object:Gem::Version
31
- version: '7.1'
31
+ version: '7.2'
32
32
  - !ruby/object:Gem::Dependency
33
33
  name: mutex_m
34
34
  requirement: !ruby/object:Gem::Requirement
@@ -179,6 +179,7 @@ files:
179
179
  - lib/shoulda/matchers/integrations/test_frameworks/missing_test_framework.rb
180
180
  - lib/shoulda/matchers/integrations/test_frameworks/rspec.rb
181
181
  - lib/shoulda/matchers/integrations/test_frameworks/test_unit.rb
182
+ - lib/shoulda/matchers/matcher_collection.rb
182
183
  - lib/shoulda/matchers/matcher_context.rb
183
184
  - lib/shoulda/matchers/rails_shim.rb
184
185
  - lib/shoulda/matchers/routing.rb
@@ -196,6 +197,7 @@ metadata:
196
197
  documentation_uri: https://matchers.shoulda.io/docs
197
198
  homepage_uri: https://matchers.shoulda.io
198
199
  source_code_uri: https://github.com/thoughtbot/shoulda-matchers
200
+ rubygems_mfa_required: 'true'
199
201
  rdoc_options: []
200
202
  require_paths:
201
203
  - lib
@@ -203,14 +205,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
203
205
  requirements:
204
206
  - - ">="
205
207
  - !ruby/object:Gem::Version
206
- version: '3.2'
208
+ version: '3.3'
207
209
  required_rubygems_version: !ruby/object:Gem::Requirement
208
210
  requirements:
209
211
  - - ">="
210
212
  - !ruby/object:Gem::Version
211
213
  version: '0'
212
214
  requirements: []
213
- rubygems_version: 3.6.9
215
+ rubygems_version: 4.0.10
214
216
  specification_version: 4
215
217
  summary: Simple one-liner tests for common Rails functionality
216
218
  test_files: []