shoulda-matchers 4.3.0 → 4.4.1

Sign up to get free protection for your applications and to get access to all the features.
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