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.
- checksums.yaml +4 -4
- data/README.md +9 -4
- data/lib/shoulda/matchers/action_controller/render_with_layout_matcher.rb +1 -5
- data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +1 -1
- data/lib/shoulda/matchers/active_model/helpers.rb +3 -1
- data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +25 -2
- data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +25 -2
- data/lib/shoulda/matchers/active_model/validate_comparison_of_matcher.rb +46 -11
- data/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +25 -2
- data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +25 -2
- data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +47 -18
- data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +26 -3
- data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +46 -11
- data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +43 -17
- data/lib/shoulda/matchers/active_model/validation_matcher.rb +4 -8
- data/lib/shoulda/matchers/active_model/validator.rb +4 -0
- data/lib/shoulda/matchers/active_record/association_matcher.rb +119 -2
- data/lib/shoulda/matchers/active_record/association_matchers/optional_matcher.rb +1 -3
- data/lib/shoulda/matchers/active_record/association_matchers/required_matcher.rb +1 -3
- data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +7 -1
- data/lib/shoulda/matchers/active_record/have_attached_matcher.rb +27 -2
- data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +26 -2
- data/lib/shoulda/matchers/active_record/uniqueness/namespace.rb +19 -2
- data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +4 -14
- data/lib/shoulda/matchers/matcher_collection.rb +99 -0
- data/lib/shoulda/matchers/rails_shim.rb +4 -4
- data/lib/shoulda/matchers/version.rb +1 -1
- data/lib/shoulda/matchers.rb +1 -0
- data/shoulda-matchers.gemspec +4 -3
- metadata +8 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 223ff92a4826a7403e0f1016553453323108899c2d2f7029873ea9f99b68a1c0
|
|
4
|
+
data.tar.gz: 2014f3ded4620ce71736c035b8f73959a2a85c1adead8067692173fbca6907de
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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', '~>
|
|
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', '~>
|
|
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.
|
|
507
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
290
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
|
210
|
-
|
|
211
|
-
|
|
233
|
+
def failure_reason
|
|
234
|
+
reason = super
|
|
212
235
|
if should_add_footnote_about_belongs_to?
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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)
|
|
@@ -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,
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
data/lib/shoulda/matchers.rb
CHANGED
|
@@ -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'
|
data/shoulda-matchers.gemspec
CHANGED
|
@@ -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.
|
|
40
|
-
s.add_dependency 'activesupport', '>= 7.
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
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: []
|