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.
- checksums.yaml +4 -4
- data/README.md +157 -77
- data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +5 -1
- data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +2 -21
- data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +27 -3
- data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +32 -0
- data/lib/shoulda/matchers/active_model/validation_matcher.rb +25 -0
- data/lib/shoulda/matchers/active_record.rb +2 -0
- data/lib/shoulda/matchers/active_record/association_matcher.rb +20 -4
- data/lib/shoulda/matchers/active_record/association_matchers/join_table_matcher.rb +2 -2
- data/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +12 -6
- data/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +20 -3
- data/lib/shoulda/matchers/active_record/have_attached_matcher.rb +147 -0
- data/lib/shoulda/matchers/active_record/have_implicit_order_column.rb +106 -0
- data/lib/shoulda/matchers/active_record/have_secure_token_matcher.rb +28 -9
- data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +2 -2
- data/lib/shoulda/matchers/independent.rb +0 -1
- data/lib/shoulda/matchers/rails_shim.rb +4 -0
- data/lib/shoulda/matchers/util.rb +9 -2
- data/lib/shoulda/matchers/util/word_wrap.rb +1 -1
- data/lib/shoulda/matchers/version.rb +1 -1
- metadata +8 -8
- data/MIT-LICENSE +0 -22
- 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
|
-
# #
|
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:
|
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(
|
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(
|
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
|
-
|
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
|
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(
|
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
|
8
|
-
:
|
9
|
-
:association_foreign_key,
|
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
|