shoulda-matchers 5.3.0 → 6.2.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +39 -15
  4. data/lib/shoulda/matchers/action_controller/permit_matcher.rb +7 -9
  5. data/lib/shoulda/matchers/action_controller/set_session_or_flash_matcher.rb +13 -15
  6. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +46 -1
  7. data/lib/shoulda/matchers/active_model/comparison_matcher.rb +157 -0
  8. data/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb +7 -0
  9. data/lib/shoulda/matchers/active_model/numericality_matchers/range_matcher.rb +1 -1
  10. data/lib/shoulda/matchers/active_model/numericality_matchers/submatchers.rb +16 -6
  11. data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +0 -6
  12. data/lib/shoulda/matchers/active_model/validate_comparison_of_matcher.rb +532 -0
  13. data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +3 -3
  14. data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +24 -11
  15. data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +64 -9
  16. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +40 -96
  17. data/lib/shoulda/matchers/active_model/validation_matcher/build_description.rb +6 -7
  18. data/lib/shoulda/matchers/active_model/validation_matcher.rb +6 -0
  19. data/lib/shoulda/matchers/active_model/validator.rb +4 -0
  20. data/lib/shoulda/matchers/active_model.rb +2 -1
  21. data/lib/shoulda/matchers/active_record/association_matcher.rb +543 -15
  22. data/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +9 -1
  23. data/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +1 -0
  24. data/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb +4 -0
  25. data/lib/shoulda/matchers/active_record/association_matchers/optional_matcher.rb +23 -19
  26. data/lib/shoulda/matchers/active_record/association_matchers/required_matcher.rb +27 -23
  27. data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +0 -8
  28. data/lib/shoulda/matchers/active_record/encrypt_matcher.rb +174 -0
  29. data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +46 -6
  30. data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +24 -13
  31. data/lib/shoulda/matchers/active_record/have_implicit_order_column.rb +3 -5
  32. data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +1 -1
  33. data/lib/shoulda/matchers/active_record/normalize_matcher.rb +151 -0
  34. data/lib/shoulda/matchers/active_record/uniqueness/model.rb +1 -1
  35. data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +82 -70
  36. data/lib/shoulda/matchers/active_record.rb +2 -0
  37. data/lib/shoulda/matchers/doublespeak/double_collection.rb +2 -6
  38. data/lib/shoulda/matchers/doublespeak/world.rb +2 -6
  39. data/lib/shoulda/matchers/independent/delegate_method_matcher.rb +13 -15
  40. data/lib/shoulda/matchers/integrations/libraries/action_controller.rb +7 -5
  41. data/lib/shoulda/matchers/integrations/libraries/routing.rb +5 -3
  42. data/lib/shoulda/matchers/rails_shim.rb +8 -6
  43. data/lib/shoulda/matchers/util/word_wrap.rb +1 -1
  44. data/lib/shoulda/matchers/util.rb +18 -20
  45. data/lib/shoulda/matchers/version.rb +1 -1
  46. data/lib/shoulda/matchers.rb +2 -2
  47. data/shoulda-matchers.gemspec +1 -1
  48. metadata +11 -8
  49. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +0 -136
@@ -13,7 +13,11 @@ module Shoulda
13
13
  end
14
14
 
15
15
  def associated_class
16
- reflection.klass
16
+ if polymorphic?
17
+ subject
18
+ else
19
+ reflection.klass
20
+ end
17
21
  end
18
22
 
19
23
  def polymorphic?
@@ -70,6 +74,10 @@ module Shoulda
70
74
  reflection.options[:through]
71
75
  end
72
76
 
77
+ def strict_loading?
78
+ reflection.options.fetch(:strict_loading, subject.strict_loading_by_default)
79
+ end
80
+
73
81
  protected
74
82
 
75
83
  attr_reader :reflection, :subject
@@ -8,6 +8,7 @@ module Shoulda
8
8
  :associated_class,
9
9
  :association_foreign_key,
10
10
  :foreign_key,
11
+ :foreign_type,
11
12
  :has_and_belongs_to_many_name,
12
13
  :join_table_name,
13
14
  :polymorphic?,
@@ -122,6 +122,10 @@ module Shoulda
122
122
  reflector.associated_class
123
123
  end
124
124
 
125
+ def actual_value_for_strict_loading
126
+ reflection.strict_loading?
127
+ end
128
+
125
129
  def actual_value_for_option(name)
126
130
  option_value = reflection.options[name]
127
131
 
@@ -22,44 +22,48 @@ module Shoulda
22
22
  if submatcher_passes?(subject)
23
23
  true
24
24
  else
25
- @missing_option = 'and for the record '
25
+ @missing_option = build_missing_option
26
26
 
27
- missing_option <<
27
+ false
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :attribute_name, :optional, :submatcher
34
+
35
+ def submatcher_passes?(subject)
36
+ if optional
37
+ submatcher.matches?(subject)
38
+ else
39
+ submatcher.does_not_match?(subject)
40
+ end
41
+ end
42
+
43
+ def build_missing_option
44
+ String.new('and for the record ').tap do |missing_option_string|
45
+ missing_option_string <<
28
46
  if optional
29
47
  'not to '
30
48
  else
31
49
  'to '
32
50
  end
33
51
 
34
- missing_option << (
52
+ missing_option_string << (
35
53
  'fail validation if '\
36
54
  ":#{attribute_name} is unset; i.e., either the association "\
37
55
  'should have been defined with `optional: '\
38
56
  "#{optional.inspect}`, or there "
39
57
  )
40
58
 
41
- missing_option <<
59
+ missing_option_string <<
42
60
  if optional
43
61
  'should not '
44
62
  else
45
63
  'should '
46
64
  end
47
65
 
48
- missing_option << "be a presence validation on :#{attribute_name}"
49
-
50
- false
51
- end
52
- end
53
-
54
- private
55
-
56
- attr_reader :attribute_name, :optional, :submatcher
57
-
58
- def submatcher_passes?(subject)
59
- if optional
60
- submatcher.matches?(subject)
61
- else
62
- submatcher.does_not_match?(subject)
66
+ missing_option_string << "be a presence validation on :#{attribute_name}"
63
67
  end
64
68
  end
65
69
  end
@@ -23,50 +23,54 @@ module Shoulda
23
23
  if submatcher_passes?(subject)
24
24
  true
25
25
  else
26
- @missing_option = 'and for the record '
26
+ @missing_option = build_missing_option
27
27
 
28
- missing_option <<
28
+ false
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :attribute_name, :required, :submatcher
35
+
36
+ def submatcher_passes?(subject)
37
+ if required
38
+ submatcher.matches?(subject)
39
+ else
40
+ submatcher.does_not_match?(subject)
41
+ end
42
+ end
43
+
44
+ def validation_message_key
45
+ :required
46
+ end
47
+
48
+ def build_missing_option
49
+ String.new('and for the record ').tap do |missing_option_string|
50
+ missing_option_string <<
29
51
  if required
30
52
  'to '
31
53
  else
32
54
  'not to '
33
55
  end
34
56
 
35
- missing_option << (
57
+ missing_option_string << (
36
58
  'fail validation if '\
37
59
  ":#{attribute_name} is unset; i.e., either the association "\
38
60
  'should have been defined with `required: '\
39
61
  "#{required.inspect}`, or there "
40
62
  )
41
63
 
42
- missing_option <<
64
+ missing_option_string <<
43
65
  if required
44
66
  'should '
45
67
  else
46
68
  'should not '
47
69
  end
48
70
 
49
- missing_option << "be a presence validation on :#{attribute_name}"
50
-
51
- false
71
+ missing_option_string << "be a presence validation on :#{attribute_name}"
52
72
  end
53
73
  end
54
-
55
- private
56
-
57
- attr_reader :attribute_name, :required, :submatcher
58
-
59
- def submatcher_passes?(subject)
60
- if required
61
- submatcher.matches?(subject)
62
- else
63
- submatcher.does_not_match?(subject)
64
- end
65
- end
66
-
67
- def validation_message_key
68
- :required
69
- end
70
74
  end
71
75
  end
72
76
  end
@@ -227,14 +227,6 @@ module Shoulda
227
227
  self
228
228
  end
229
229
 
230
- def with(expected_enum_values)
231
- Shoulda::Matchers.warn_about_deprecated_method(
232
- 'The `with` qualifier on `define_enum_for`',
233
- '`with_values`',
234
- )
235
- with_values(expected_enum_values)
236
- end
237
-
238
230
  def with_prefix(expected_prefix = true)
239
231
  options[:prefix] = expected_prefix
240
232
  self
@@ -0,0 +1,174 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module ActiveRecord
4
+ # The `encrypt` matcher tests usage of the
5
+ # `encrypts` macro (Rails 7+ only).
6
+ #
7
+ # class Survey < ActiveRecord::Base
8
+ # encrypts :access_code
9
+ # end
10
+ #
11
+ # # RSpec
12
+ # RSpec.describe Survey, type: :model do
13
+ # it { should encrypt(:access_code) }
14
+ # end
15
+ #
16
+ # # Minitest (Shoulda)
17
+ # class SurveyTest < ActiveSupport::TestCase
18
+ # should encrypt(:access_code)
19
+ # end
20
+ #
21
+ # #### Qualifiers
22
+ #
23
+ # ##### deterministic
24
+ #
25
+ # class Survey < ActiveRecord::Base
26
+ # encrypts :access_code, deterministic: true
27
+ # end
28
+ #
29
+ # # RSpec
30
+ # RSpec.describe Survey, type: :model do
31
+ # it { should encrypt(:access_code).deterministic(true) }
32
+ # end
33
+ #
34
+ # # Minitest (Shoulda)
35
+ # class SurveyTest < ActiveSupport::TestCase
36
+ # should encrypt(:access_code).deterministic(true)
37
+ # end
38
+ #
39
+ # ##### downcase
40
+ #
41
+ # class Survey < ActiveRecord::Base
42
+ # encrypts :access_code, downcase: true
43
+ # end
44
+ #
45
+ # # RSpec
46
+ # RSpec.describe Survey, type: :model do
47
+ # it { should encrypt(:access_code).downcase(true) }
48
+ # end
49
+ #
50
+ # # Minitest (Shoulda)
51
+ # class SurveyTest < ActiveSupport::TestCase
52
+ # should encrypt(:access_code).downcase(true)
53
+ # end
54
+ #
55
+ # ##### ignore_case
56
+ #
57
+ # class Survey < ActiveRecord::Base
58
+ # encrypts :access_code, deterministic: true, ignore_case: true
59
+ # end
60
+ #
61
+ # # RSpec
62
+ # RSpec.describe Survey, type: :model do
63
+ # it { should encrypt(:access_code).ignore_case(true) }
64
+ # end
65
+ #
66
+ # # Minitest (Shoulda)
67
+ # class SurveyTest < ActiveSupport::TestCase
68
+ # should encrypt(:access_code).ignore_case(true)
69
+ # end
70
+ #
71
+ # @return [EncryptMatcher]
72
+ #
73
+ def encrypt(value)
74
+ EncryptMatcher.new(value)
75
+ end
76
+
77
+ # @private
78
+ class EncryptMatcher
79
+ def initialize(attribute)
80
+ @attribute = attribute.to_sym
81
+ @options = {}
82
+ end
83
+
84
+ attr_reader :failure_message, :failure_message_when_negated
85
+
86
+ def deterministic(deterministic)
87
+ with_option(:deterministic, deterministic)
88
+ end
89
+
90
+ def downcase(downcase)
91
+ with_option(:downcase, downcase)
92
+ end
93
+
94
+ def ignore_case(ignore_case)
95
+ with_option(:ignore_case, ignore_case)
96
+ end
97
+
98
+ def matches?(subject)
99
+ @subject = subject
100
+ result = encrypted_attributes_included? &&
101
+ options_correct?(
102
+ :deterministic,
103
+ :downcase,
104
+ :ignore_case,
105
+ )
106
+
107
+ if result
108
+ @failure_message_when_negated = "Did not expect to #{description} of #{class_name}"
109
+ if @options.present?
110
+ @failure_message_when_negated += "
111
+ using "
112
+ @failure_message_when_negated += @options.map { |opt, expected|
113
+ ":#{opt} option as ‹#{expected}›"
114
+ }.join(' and
115
+ ')
116
+ end
117
+
118
+ @failure_message_when_negated += ",
119
+ but it did"
120
+ end
121
+
122
+ result
123
+ end
124
+
125
+ def description
126
+ "encrypt :#{@attribute}"
127
+ end
128
+
129
+ private
130
+
131
+ def encrypted_attributes_included?
132
+ if encrypted_attributes.include?(@attribute)
133
+ true
134
+ else
135
+ @failure_message = "Expected to #{description} of #{class_name}, but it did not"
136
+ false
137
+ end
138
+ end
139
+
140
+ def with_option(option_name, value)
141
+ @options[option_name] = value
142
+ self
143
+ end
144
+
145
+ def options_correct?(*opts)
146
+ opts.all? do |opt|
147
+ next true unless @options.key?(opt)
148
+
149
+ expected = @options[opt]
150
+ actual = encrypted_attribute_scheme.send("#{opt}?")
151
+ next true if expected == actual
152
+
153
+ @failure_message = "Expected to #{description} of #{class_name} using :#{opt} option
154
+ as ‹#{expected}›, but got ‹#{actual}›"
155
+
156
+ false
157
+ end
158
+ end
159
+
160
+ def encrypted_attributes
161
+ @_encrypted_attributes ||= @subject.class.encrypted_attributes || []
162
+ end
163
+
164
+ def encrypted_attribute_scheme
165
+ @subject.class.type_for_attribute(@attribute).scheme
166
+ end
167
+
168
+ def class_name
169
+ @subject.class.name
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -48,6 +48,30 @@ module Shoulda
48
48
  # should have_db_column(:camera_aperture).of_type(:decimal)
49
49
  # end
50
50
  #
51
+ # ##### of_sql_type
52
+ #
53
+ # Use `of_sql_type` to assert that a column is defined as a certain sql_type.
54
+ #
55
+ # class CreatePhones < ActiveRecord::Migration
56
+ # def change
57
+ # create_table :phones do |t|
58
+ # t.string :camera_aperture, limit: 36
59
+ # end
60
+ # end
61
+ # end
62
+ #
63
+ # # RSpec
64
+ # RSpec.describe Phone, type: :model do
65
+ # it do
66
+ # should have_db_column(:camera_aperture).of_sql_type('varchar(36)')
67
+ # end
68
+ # end
69
+ #
70
+ # # Minitest (Shoulda)
71
+ # class PhoneTest < ActiveSupport::TestCase
72
+ # should have_db_column(:camera_aperture).of_sql_type('varchar(36)')
73
+ # end
74
+ #
51
75
  # ##### with_options
52
76
  #
53
77
  # Use `with_options` to assert that a column has been defined with
@@ -96,6 +120,11 @@ module Shoulda
96
120
  self
97
121
  end
98
122
 
123
+ def of_sql_type(sql_column_type)
124
+ @options[:sql_column_type] = sql_column_type
125
+ self
126
+ end
127
+
99
128
  def with_options(opts = {})
100
129
  validate_options(opts)
101
130
  OPTIONS.each do |attribute|
@@ -110,6 +139,7 @@ module Shoulda
110
139
  @subject = subject
111
140
  column_exists? &&
112
141
  correct_column_type? &&
142
+ correct_sql_column_type? &&
113
143
  correct_precision? &&
114
144
  correct_limit? &&
115
145
  correct_default? &&
@@ -129,12 +159,9 @@ module Shoulda
129
159
 
130
160
  def description
131
161
  desc = "have db column named #{@column}"
132
- if @options.key?(:column_type)
133
- desc << " of type #{@options[:column_type]}"
134
- end
135
- if @options.key?(:precision)
136
- desc << " of precision #{@options[:precision]}"
137
- end
162
+ desc << " of type #{@options[:column_type]}" if @options.key?(:column_type)
163
+ desc << " of sql_type #{@options[:sql_column_type]}" if @options.key?(:sql_column_type)
164
+ desc << " of precision #{@options[:precision]}" if @options.key?(:precision)
138
165
  desc << " of limit #{@options[:limit]}" if @options.key?(:limit)
139
166
  desc << " of default #{@options[:default]}" if @options.key?(:default)
140
167
  desc << " of null #{@options[:null]}" if @options.key?(:null)
@@ -178,6 +205,19 @@ module Shoulda
178
205
  end
179
206
  end
180
207
 
208
+ def correct_sql_column_type?
209
+ return true unless @options.key?(:sql_column_type)
210
+
211
+ if matched_column.sql_type.to_s == @options[:sql_column_type].to_s
212
+ true
213
+ else
214
+ @missing =
215
+ "#{model_class} has a db column named #{@column} " <<
216
+ "of sql type #{matched_column.sql_type}, not #{@options[:sql_column_type]}."
217
+ false
218
+ end
219
+ end
220
+
181
221
  def correct_precision?
182
222
  return true unless @options.key?(:precision)
183
223
 
@@ -152,18 +152,18 @@ module Shoulda
152
152
  end
153
153
 
154
154
  def description
155
- description = 'have '
156
-
157
- description <<
158
- if qualifiers.include?(:unique)
159
- "#{Shoulda::Matchers::Util.a_or_an(index_type)} "
160
- else
161
- 'an '
162
- end
155
+ String.new('have ').tap do |description|
156
+ description <<
157
+ if qualifiers.include?(:unique)
158
+ "#{Shoulda::Matchers::Util.a_or_an(index_type)} "
159
+ else
160
+ 'an '
161
+ end
163
162
 
164
- description << 'index on '
163
+ description << 'index on '
165
164
 
166
- description << inspected_expected_columns
165
+ description << inspected_expected_columns
166
+ end
167
167
  end
168
168
 
169
169
  private
@@ -197,17 +197,28 @@ module Shoulda
197
197
  def matched_index
198
198
  @_matched_index ||=
199
199
  if expected_columns.one?
200
- actual_indexes.detect do |index|
200
+ sorted_indexes.detect do |index|
201
201
  Array.wrap(index.columns) == expected_columns
202
202
  end
203
203
  else
204
- actual_indexes.detect do |index|
204
+ sorted_indexes.detect do |index|
205
205
  index.columns == expected_columns
206
206
  end
207
207
  end
208
208
  end
209
209
 
210
- def actual_indexes
210
+ def sorted_indexes
211
+ if qualifiers.include?(:unique)
212
+ # return indexes with unique matching the qualifier first
213
+ unsorted_indexes.sort_by do |index|
214
+ index.unique == qualifiers[:unique] ? 0 : 1
215
+ end
216
+ else
217
+ unsorted_indexes
218
+ end
219
+ end
220
+
221
+ def unsorted_indexes
211
222
  model.connection.indexes(table_name)
212
223
  end
213
224
 
@@ -2,7 +2,7 @@ module Shoulda
2
2
  module Matchers
3
3
  module ActiveRecord
4
4
  # The `have_implicit_order_column` matcher tests that the model has `implicit_order_column`
5
- # assigned to one of the table columns. (Rails 6+ only)
5
+ # assigned to one of the table columns.
6
6
  #
7
7
  # class Product < ApplicationRecord
8
8
  # self.implicit_order_column = :created_at
@@ -20,10 +20,8 @@ module Shoulda
20
20
  #
21
21
  # @return [HaveImplicitOrderColumnMatcher]
22
22
  #
23
- if RailsShim.active_record_gte_6?
24
- def have_implicit_order_column(column_name)
25
- HaveImplicitOrderColumnMatcher.new(column_name)
26
- end
23
+ def have_implicit_order_column(column_name)
24
+ HaveImplicitOrderColumnMatcher.new(column_name)
27
25
  end
28
26
 
29
27
  # @private
@@ -59,7 +59,7 @@ module Shoulda
59
59
  private
60
60
 
61
61
  def readonly_attributes
62
- @_readonly_attributes ||= (@subject.class.readonly_attributes || [])
62
+ @_readonly_attributes ||= @subject.class.readonly_attributes || []
63
63
  end
64
64
 
65
65
  def class_name