shoulda-matchers 4.3.0 → 4.4.1

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 (24) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +157 -77
  3. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +5 -1
  4. data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +2 -21
  5. data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +27 -3
  6. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +32 -0
  7. data/lib/shoulda/matchers/active_model/validation_matcher.rb +25 -0
  8. data/lib/shoulda/matchers/active_record.rb +2 -0
  9. data/lib/shoulda/matchers/active_record/association_matcher.rb +20 -4
  10. data/lib/shoulda/matchers/active_record/association_matchers/join_table_matcher.rb +2 -2
  11. data/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +12 -6
  12. data/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +20 -3
  13. data/lib/shoulda/matchers/active_record/have_attached_matcher.rb +147 -0
  14. data/lib/shoulda/matchers/active_record/have_implicit_order_column.rb +106 -0
  15. data/lib/shoulda/matchers/active_record/have_secure_token_matcher.rb +28 -9
  16. data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +2 -2
  17. data/lib/shoulda/matchers/independent.rb +0 -1
  18. data/lib/shoulda/matchers/rails_shim.rb +4 -0
  19. data/lib/shoulda/matchers/util.rb +9 -2
  20. data/lib/shoulda/matchers/util/word_wrap.rb +1 -1
  21. data/lib/shoulda/matchers/version.rb +1 -1
  22. metadata +8 -8
  23. data/MIT-LICENSE +0 -22
  24. data/lib/shoulda/matchers/independent/delegate_method_matcher/stubbed_target.rb +0 -37
@@ -232,13 +232,34 @@ module Shoulda
232
232
  # it { should validate_length_of(:bio).is_at_least(15).allow_nil }
233
233
  # end
234
234
  #
235
- # # Test::Unit
235
+ # # Minitest (Shoulda)
236
236
  # class UserTest < ActiveSupport::TestCase
237
237
  # should validate_length_of(:bio).is_at_least(15).allow_nil
238
238
  # end
239
239
  #
240
240
  # @return [ValidateLengthOfMatcher]
241
241
  #
242
+ # # ##### allow_blank
243
+ #
244
+ # Use `allow_blank` to assert that the attribute allows blank.
245
+ #
246
+ # class User
247
+ # include ActiveModel::Model
248
+ # attr_accessor :bio
249
+ #
250
+ # validates_length_of :bio, minimum: 15, allow_blank: true
251
+ # end
252
+ #
253
+ # # RSpec
254
+ # describe User do
255
+ # it { should validate_length_of(:bio).is_at_least(15).allow_blank }
256
+ # end
257
+ #
258
+ # # Minitest (Shoulda)
259
+ # class UserTest < ActiveSupport::TestCase
260
+ # should validate_length_of(:bio).is_at_least(15).allow_blank
261
+ # end
262
+ #
242
263
  def validate_length_of(attr)
243
264
  ValidateLengthOfMatcher.new(attr)
244
265
  end
@@ -331,7 +352,8 @@ module Shoulda
331
352
 
332
353
  lower_bound_matches? &&
333
354
  upper_bound_matches? &&
334
- allow_nil_matches?
355
+ allow_nil_matches? &&
356
+ allow_blank_matches?
335
357
  end
336
358
 
337
359
  def does_not_match?(subject)
@@ -339,7 +361,8 @@ module Shoulda
339
361
 
340
362
  lower_bound_does_not_match? ||
341
363
  upper_bound_does_not_match? ||
342
- allow_nil_does_not_match?
364
+ allow_nil_does_not_match? ||
365
+ allow_blank_does_not_match?
343
366
  end
344
367
 
345
368
  private
@@ -376,6 +399,7 @@ module Shoulda
376
399
  def disallows_lower_length?
377
400
  !@options.key?(:minimum) ||
378
401
  @options[:minimum] == 0 ||
402
+ (@options[:minimum] == 1 && expects_to_allow_blank?) ||
379
403
  disallows_length_of?(
380
404
  @options[:minimum] - 1,
381
405
  translated_short_message
@@ -204,6 +204,33 @@ module Shoulda
204
204
  # is_greater_than(21)
205
205
  # end
206
206
  #
207
+ # ##### is_other_than
208
+ #
209
+ # Use `is_other_than` to test usage of the `:other_than` option.
210
+ # This asserts that the attribute can take a number which is not equal to
211
+ # the given value.
212
+ #
213
+ # class Person
214
+ # include ActiveModel::Model
215
+ # attr_accessor :legal_age
216
+ #
217
+ # validates_numericality_of :legal_age, other_than: 21
218
+ # end
219
+ #
220
+ # # RSpec
221
+ # RSpec.describe Person, type: :model do
222
+ # it do
223
+ # should validate_numericality_of(:legal_age).
224
+ # is_other_than(21)
225
+ # end
226
+ # end
227
+ #
228
+ # # Minitest (Shoulda)
229
+ # class PersonTest < ActiveSupport::TestCase
230
+ # should validate_numericality_of(:legal_age).
231
+ # is_other_than(21)
232
+ # end
233
+ #
207
234
  # ##### even
208
235
  #
209
236
  # Use `even` to test usage of the `:even` option. This asserts that the
@@ -395,6 +422,11 @@ module Shoulda
395
422
  self
396
423
  end
397
424
 
425
+ def is_other_than(value)
426
+ prepare_submatcher(comparison_matcher_for(value, :!=).for(@attribute))
427
+ self
428
+ end
429
+
398
430
  def with_message(message)
399
431
  @expects_custom_validation_message = true
400
432
  @expected_message = message
@@ -24,6 +24,11 @@ module Shoulda
24
24
  self
25
25
  end
26
26
 
27
+ def allow_blank
28
+ options[:allow_blank] = true
29
+ self
30
+ end
31
+
27
32
  def strict
28
33
  @expects_strict = true
29
34
  self
@@ -116,8 +121,20 @@ module Shoulda
116
121
  )
117
122
  end
118
123
 
124
+ def allow_blank_matches?
125
+ !expects_to_allow_blank? ||
126
+ blank_values.all? { |value| allows_value_of(value) }
127
+ end
128
+
129
+ def allow_blank_does_not_match?
130
+ expects_to_allow_blank? &&
131
+ blank_values.all? { |value| disallows_value_of(value) }
132
+ end
133
+
119
134
  private
120
135
 
136
+ attr_reader :options
137
+
121
138
  def overall_failure_message
122
139
  Shoulda::Matchers.word_wrap(
123
140
  "Expected #{model.name} to #{description}, but this could not be " +
@@ -161,6 +178,14 @@ module Shoulda
161
178
  @last_submatcher_run = matcher
162
179
  matcher.matches?(subject)
163
180
  end
181
+
182
+ def expects_to_allow_blank?
183
+ options[:allow_blank]
184
+ end
185
+
186
+ def blank_values
187
+ ['', ' ', "\n", "\r", "\t", "\f"]
188
+ end
164
189
  end
165
190
  end
166
191
  end
@@ -14,6 +14,7 @@ require "shoulda/matchers/active_record/association_matchers/model_reflection"
14
14
  require "shoulda/matchers/active_record/association_matchers/option_verifier"
15
15
  require "shoulda/matchers/active_record/have_db_column_matcher"
16
16
  require "shoulda/matchers/active_record/have_db_index_matcher"
17
+ require "shoulda/matchers/active_record/have_implicit_order_column"
17
18
  require "shoulda/matchers/active_record/have_readonly_attribute_matcher"
18
19
  require "shoulda/matchers/active_record/have_rich_text_matcher"
19
20
  require "shoulda/matchers/active_record/have_secure_token_matcher"
@@ -22,6 +23,7 @@ require "shoulda/matchers/active_record/accept_nested_attributes_for_matcher"
22
23
  require "shoulda/matchers/active_record/define_enum_for_matcher"
23
24
  require "shoulda/matchers/active_record/uniqueness"
24
25
  require "shoulda/matchers/active_record/validate_uniqueness_of_matcher"
26
+ require "shoulda/matchers/active_record/have_attached_matcher"
25
27
 
26
28
  module Shoulda
27
29
  module Matchers
@@ -920,21 +920,21 @@ module Shoulda
920
920
  # asserts that the table you're referring to actually exists.
921
921
  #
922
922
  # class Person < ActiveRecord::Base
923
- # has_and_belongs_to_many :issues, join_table: 'people_tickets'
923
+ # has_and_belongs_to_many :issues, join_table: :people_tickets
924
924
  # end
925
925
  #
926
926
  # # RSpec
927
927
  # RSpec.describe Person, type: :model do
928
928
  # it do
929
929
  # should have_and_belong_to_many(:issues).
930
- # join_table('people_tickets')
930
+ # join_table(:people_tickets)
931
931
  # end
932
932
  # end
933
933
  #
934
934
  # # Minitest (Shoulda)
935
935
  # class PersonTest < ActiveSupport::TestCase
936
936
  # should have_and_belong_to_many(:issues).
937
- # join_table('people_tickets')
937
+ # join_table(:people_tickets)
938
938
  # end
939
939
  #
940
940
  # ##### validate
@@ -1144,6 +1144,7 @@ module Shoulda
1144
1144
  @subject = subject
1145
1145
  association_exists? &&
1146
1146
  macro_correct? &&
1147
+ validate_inverse_of_through_association &&
1147
1148
  (polymorphic? || class_exists?) &&
1148
1149
  foreign_key_exists? &&
1149
1150
  primary_key_exists? &&
@@ -1198,7 +1199,14 @@ module Shoulda
1198
1199
  end
1199
1200
 
1200
1201
  def expectation
1201
- "#{model_class.name} to have a #{macro} association called #{name}"
1202
+ expectation =
1203
+ "#{model_class.name} to have a #{macro} association called #{name}"
1204
+
1205
+ if through?
1206
+ expectation << " through #{reflector.has_and_belongs_to_many_name}"
1207
+ end
1208
+
1209
+ expectation
1202
1210
  end
1203
1211
 
1204
1212
  def missing_options
@@ -1241,6 +1249,14 @@ module Shoulda
1241
1249
  end
1242
1250
  end
1243
1251
 
1252
+ def validate_inverse_of_through_association
1253
+ reflector.validate_inverse_of_through_association!
1254
+ true
1255
+ rescue ::ActiveRecord::ActiveRecordError => error
1256
+ @missing = error.message
1257
+ false
1258
+ end
1259
+
1244
1260
  def macro_supports_primary_key?
1245
1261
  macro == :belongs_to ||
1246
1262
  ([:has_many, :has_one].include?(macro) && !through?)
@@ -29,7 +29,7 @@ module Shoulda
29
29
  if option_verifier.correct_for_string?(:join_table, options[:join_table_name])
30
30
  true
31
31
  else
32
- @failure_message = "#{name} should use '#{options[:join_table_name]}' for :join_table option"
32
+ @failure_message = "#{name} should use #{options[:join_table_name].inspect} for :join_table option"
33
33
  false
34
34
  end
35
35
  else
@@ -38,7 +38,7 @@ module Shoulda
38
38
  end
39
39
 
40
40
  def join_table_exists?
41
- if RailsShim.tables_and_views(connection).include?(join_table_name)
41
+ if RailsShim.tables_and_views(connection).include?(join_table_name.to_s)
42
42
  true
43
43
  else
44
44
  @failure_message = missing_table_message
@@ -35,12 +35,12 @@ module Shoulda
35
35
  join_table_name.to_s
36
36
  end
37
37
 
38
- def association_relation
38
+ def association_relation(related_instance)
39
39
  relation = associated_class.all
40
40
 
41
41
  if reflection.scope
42
42
  # Source: AR::Associations::AssociationScope#eval_scope
43
- relation.instance_exec(subject, &reflection.scope)
43
+ relation.instance_exec(related_instance, &reflection.scope)
44
44
  else
45
45
  relation
46
46
  end
@@ -65,16 +65,22 @@ module Shoulda
65
65
  end
66
66
  end
67
67
 
68
+ def validate_inverse_of_through_association!
69
+ if through?
70
+ reflection.check_validity!
71
+ end
72
+ end
73
+
74
+ def has_and_belongs_to_many_name
75
+ reflection.options[:through]
76
+ end
77
+
68
78
  protected
69
79
 
70
80
  attr_reader :reflection, :subject
71
81
 
72
82
  private
73
83
 
74
- def has_and_belongs_to_many_name
75
- reflection.options[:through]
76
- end
77
-
78
84
  def has_and_belongs_to_many_name_table_name
79
85
  if has_and_belongs_to_many_reflection
80
86
  has_and_belongs_to_many_reflection.table_name
@@ -4,15 +4,32 @@ module Shoulda
4
4
  module AssociationMatchers
5
5
  # @private
6
6
  class ModelReflector
7
- delegate :associated_class, :through?, :join_table_name,
8
- :association_relation, :polymorphic?, :foreign_key,
9
- :association_foreign_key, to: :reflection
7
+ delegate(
8
+ :associated_class,
9
+ :association_foreign_key,
10
+ :foreign_key,
11
+ :has_and_belongs_to_many_name,
12
+ :join_table_name,
13
+ :polymorphic?,
14
+ :validate_inverse_of_through_association!,
15
+ to: :reflection
16
+ )
17
+
18
+ delegate(
19
+ :through?,
20
+ to: :reflection,
21
+ allow_nil: true
22
+ )
10
23
 
11
24
  def initialize(subject, name)
12
25
  @subject = subject
13
26
  @name = name
14
27
  end
15
28
 
29
+ def association_relation
30
+ reflection.association_relation(subject)
31
+ end
32
+
16
33
  def reflection
17
34
  @reflection ||= reflect_on_association(name)
18
35
  end
@@ -0,0 +1,147 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module ActiveRecord
4
+ def have_one_attached(name)
5
+ HaveAttachedMatcher.new(:one, name)
6
+ end
7
+
8
+ def have_many_attached(name)
9
+ HaveAttachedMatcher.new(:many, name)
10
+ end
11
+
12
+ # @private
13
+ class HaveAttachedMatcher
14
+ attr_reader :name
15
+
16
+ def initialize(macro, name)
17
+ @macro = macro
18
+ @name = name
19
+ end
20
+
21
+ def description
22
+ "have a has_#{macro}_attached called #{name}"
23
+ end
24
+
25
+ def failure_message
26
+ <<-MESSAGE
27
+ Expected #{expectation}, but this could not be proved.
28
+ #{@failure}
29
+ MESSAGE
30
+ end
31
+
32
+ def failure_message_when_negated
33
+ <<-MESSAGE
34
+ Did not expect #{expectation}, but it does.
35
+ MESSAGE
36
+ end
37
+
38
+ def expectation
39
+ "#{model_class.name} to #{description}"
40
+ end
41
+
42
+ def matches?(subject)
43
+ @subject = subject
44
+ reader_attribute_exists? &&
45
+ writer_attribute_exists? &&
46
+ attachments_association_exists? &&
47
+ blobs_association_exists? &&
48
+ eager_loading_scope_exists?
49
+ end
50
+
51
+ private
52
+
53
+ attr_reader :subject, :macro
54
+
55
+ def reader_attribute_exists?
56
+ if subject.respond_to?(name)
57
+ true
58
+ else
59
+ @failure = "#{model_class.name} does not have a :#{name} method."
60
+ false
61
+ end
62
+ end
63
+
64
+ def writer_attribute_exists?
65
+ if subject.respond_to?("#{name}=")
66
+ true
67
+ else
68
+ @failure = "#{model_class.name} does not have a :#{name}= method."
69
+ false
70
+ end
71
+ end
72
+
73
+ def attachments_association_exists?
74
+ if attachments_association_matcher.matches?(subject)
75
+ true
76
+ else
77
+ @failure = attachments_association_matcher.failure_message
78
+ false
79
+ end
80
+ end
81
+
82
+ def attachments_association_matcher
83
+ @_attachments_association_matcher ||=
84
+ AssociationMatcher.new(
85
+ :"has_#{macro}",
86
+ attachments_association_name,
87
+ ).
88
+ conditions(name: name).
89
+ class_name('ActiveStorage::Attachment').
90
+ inverse_of(:record)
91
+ end
92
+
93
+ def attachments_association_name
94
+ case macro
95
+ when :one then
96
+ "#{name}_attachment"
97
+ when :many then
98
+ "#{name}_attachments"
99
+ end
100
+ end
101
+
102
+ def blobs_association_exists?
103
+ if blobs_association_matcher.matches?(subject)
104
+ true
105
+ else
106
+ @failure = blobs_association_matcher.failure_message
107
+ false
108
+ end
109
+ end
110
+
111
+ def blobs_association_matcher
112
+ @_blobs_association_matcher ||=
113
+ AssociationMatcher.new(
114
+ :"has_#{macro}",
115
+ blobs_association_name,
116
+ ).
117
+ through(attachments_association_name).
118
+ class_name('ActiveStorage::Blob').
119
+ source(:blob)
120
+ end
121
+
122
+ def blobs_association_name
123
+ case macro
124
+ when :one then
125
+ "#{name}_blob"
126
+ when :many then
127
+ "#{name}_blobs"
128
+ end
129
+ end
130
+
131
+ def eager_loading_scope_exists?
132
+ if model_class.respond_to?("with_attached_#{name}")
133
+ true
134
+ else
135
+ @failure = "#{model_class.name} does not have a " \
136
+ ":with_attached_#{name} scope."
137
+ false
138
+ end
139
+ end
140
+
141
+ def model_class
142
+ subject.class
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end