shoulda-matchers 4.0.1 → 4.1.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 +189 -87
- data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +1 -0
- data/lib/shoulda/matchers/active_model/qualifiers.rb +1 -0
- data/lib/shoulda/matchers/active_model/qualifiers/allow_nil.rb +26 -0
- data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +2 -1
- data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +171 -25
- data/lib/shoulda/matchers/active_record/association_matcher.rb +11 -7
- data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +120 -63
- data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +161 -45
- data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +3 -13
- data/lib/shoulda/matchers/configuration.rb +12 -1
- data/lib/shoulda/matchers/integrations/configuration.rb +7 -3
- data/lib/shoulda/matchers/rails_shim.rb +37 -0
- data/lib/shoulda/matchers/util.rb +1 -1
- data/lib/shoulda/matchers/util/word_wrap.rb +4 -3
- data/lib/shoulda/matchers/version.rb +1 -1
- data/shoulda-matchers.gemspec +20 -13
- metadata +9 -6
@@ -272,7 +272,7 @@ module Shoulda
|
|
272
272
|
# should belong_to(:organization).required
|
273
273
|
# end
|
274
274
|
#
|
275
|
-
#
|
275
|
+
# ##### without_validating_presence
|
276
276
|
#
|
277
277
|
# Use `without_validating_presence` with `belong_to` to prevent the
|
278
278
|
# matcher from checking whether the association disallows nil (Rails 5+
|
@@ -1362,13 +1362,11 @@ module Shoulda
|
|
1362
1362
|
def class_has_foreign_key?(klass)
|
1363
1363
|
if options.key?(:foreign_key)
|
1364
1364
|
option_verifier.correct_for_string?(:foreign_key, options[:foreign_key])
|
1365
|
+
elsif column_names_for(klass).include?(foreign_key)
|
1366
|
+
true
|
1365
1367
|
else
|
1366
|
-
|
1367
|
-
|
1368
|
-
else
|
1369
|
-
@missing = "#{klass} does not have a #{foreign_key} foreign key."
|
1370
|
-
false
|
1371
|
-
end
|
1368
|
+
@missing = "#{klass} does not have a #{foreign_key} foreign key."
|
1369
|
+
false
|
1372
1370
|
end
|
1373
1371
|
end
|
1374
1372
|
|
@@ -1407,6 +1405,12 @@ module Shoulda
|
|
1407
1405
|
failing_submatchers.empty?
|
1408
1406
|
end
|
1409
1407
|
|
1408
|
+
def column_names_for(klass)
|
1409
|
+
klass.column_names
|
1410
|
+
rescue ::ActiveRecord::StatementInvalid
|
1411
|
+
[]
|
1412
|
+
end
|
1413
|
+
|
1410
1414
|
def belongs_to_required_by_default?
|
1411
1415
|
::ActiveRecord::Base.belongs_to_required_by_default
|
1412
1416
|
end
|
@@ -144,31 +144,22 @@ module Shoulda
|
|
144
144
|
end
|
145
145
|
|
146
146
|
def description
|
147
|
-
description = "
|
147
|
+
description = "#{simple_description} backed by "
|
148
148
|
description << Shoulda::Matchers::Util.a_or_an(expected_column_type)
|
149
149
|
|
150
|
-
if
|
151
|
-
description << '
|
152
|
-
description <<
|
150
|
+
if expected_enum_values.any?
|
151
|
+
description << ' with values '
|
152
|
+
description << Shoulda::Matchers::Util.inspect_value(
|
153
|
+
expected_enum_values,
|
154
|
+
)
|
153
155
|
end
|
154
156
|
|
155
|
-
if options[:
|
156
|
-
|
157
|
-
description << ' and'
|
158
|
-
else
|
159
|
-
description << ', using'
|
160
|
-
end
|
161
|
-
|
162
|
-
description << ' a suffix of '
|
163
|
-
|
164
|
-
description << "#{options[:expected_suffix].inspect}"
|
157
|
+
if options[:prefix]
|
158
|
+
description << ", prefix: #{options[:prefix].inspect}"
|
165
159
|
end
|
166
160
|
|
167
|
-
if
|
168
|
-
description <<
|
169
|
-
description << Shoulda::Matchers::Util.inspect_value(
|
170
|
-
presented_expected_enum_values,
|
171
|
-
)
|
161
|
+
if options[:suffix]
|
162
|
+
description << ", suffix: #{options[:suffix].inspect}"
|
172
163
|
end
|
173
164
|
|
174
165
|
description
|
@@ -187,13 +178,13 @@ module Shoulda
|
|
187
178
|
with_values(expected_enum_values)
|
188
179
|
end
|
189
180
|
|
190
|
-
def with_prefix(expected_prefix =
|
191
|
-
options[:
|
181
|
+
def with_prefix(expected_prefix = true)
|
182
|
+
options[:prefix] = expected_prefix
|
192
183
|
self
|
193
184
|
end
|
194
185
|
|
195
|
-
def with_suffix(expected_suffix =
|
196
|
-
options[:
|
186
|
+
def with_suffix(expected_suffix = true)
|
187
|
+
options[:suffix] = expected_suffix
|
197
188
|
self
|
198
189
|
end
|
199
190
|
|
@@ -212,13 +203,14 @@ module Shoulda
|
|
212
203
|
end
|
213
204
|
|
214
205
|
def failure_message
|
215
|
-
message =
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
206
|
+
message =
|
207
|
+
if enum_defined?
|
208
|
+
"Expected #{model} to #{expectation}. "
|
209
|
+
else
|
210
|
+
"Expected #{model} to #{expectation}, but "
|
211
|
+
end
|
220
212
|
|
221
|
-
message << '.'
|
213
|
+
message << failure_message_continuation + '.'
|
222
214
|
|
223
215
|
Shoulda::Matchers.word_wrap(message)
|
224
216
|
end
|
@@ -230,20 +222,65 @@ module Shoulda
|
|
230
222
|
|
231
223
|
private
|
232
224
|
|
233
|
-
attr_reader :attribute_name, :options, :record,
|
225
|
+
attr_reader :attribute_name, :options, :record,
|
226
|
+
:failure_message_continuation
|
234
227
|
|
235
228
|
def expectation
|
236
|
-
|
237
|
-
|
229
|
+
if enum_defined?
|
230
|
+
expectation = "#{simple_description} backed by "
|
231
|
+
expectation << Shoulda::Matchers::Util.a_or_an(expected_column_type)
|
232
|
+
|
233
|
+
if expected_enum_values.any?
|
234
|
+
expectation << ', mapping '
|
235
|
+
expectation << presented_enum_mapping(
|
236
|
+
normalized_expected_enum_values,
|
237
|
+
)
|
238
|
+
end
|
239
|
+
|
240
|
+
if expected_prefix
|
241
|
+
expectation <<
|
242
|
+
if expected_suffix
|
243
|
+
', '
|
244
|
+
else
|
245
|
+
' and '
|
246
|
+
end
|
247
|
+
|
248
|
+
expectation << 'prefixing accessor methods with '
|
249
|
+
expectation << "#{expected_prefix}_".inspect
|
250
|
+
end
|
251
|
+
|
252
|
+
if expected_suffix
|
253
|
+
expectation <<
|
254
|
+
if expected_prefix
|
255
|
+
', and '
|
256
|
+
else
|
257
|
+
' and '
|
258
|
+
end
|
259
|
+
|
260
|
+
expectation << 'suffixing accessor methods with '
|
261
|
+
expectation << "_#{expected_suffix}".inspect
|
262
|
+
end
|
238
263
|
|
239
|
-
|
240
|
-
if expected_enum_values.is_a?(Hash)
|
241
|
-
expected_enum_values.symbolize_keys
|
264
|
+
expectation
|
242
265
|
else
|
243
|
-
|
266
|
+
simple_description
|
244
267
|
end
|
245
268
|
end
|
246
269
|
|
270
|
+
def simple_description
|
271
|
+
"define :#{attribute_name} as an enum"
|
272
|
+
end
|
273
|
+
|
274
|
+
def presented_enum_mapping(enum_values)
|
275
|
+
enum_values.
|
276
|
+
map { |output_to_input|
|
277
|
+
output_to_input.
|
278
|
+
map(&Shoulda::Matchers::Util.method(:inspect_value)).
|
279
|
+
join(' to ')
|
280
|
+
}.
|
281
|
+
to_sentence
|
282
|
+
end
|
283
|
+
|
247
284
|
def normalized_expected_enum_values
|
248
285
|
to_hash(expected_enum_values)
|
249
286
|
end
|
@@ -256,14 +293,6 @@ module Shoulda
|
|
256
293
|
options[:expected_enum_values]
|
257
294
|
end
|
258
295
|
|
259
|
-
def presented_actual_enum_values
|
260
|
-
if expected_enum_values.is_a?(Array)
|
261
|
-
to_array(actual_enum_values)
|
262
|
-
else
|
263
|
-
to_hash(actual_enum_values).symbolize_keys
|
264
|
-
end
|
265
|
-
end
|
266
|
-
|
267
296
|
def normalized_actual_enum_values
|
268
297
|
to_hash(actual_enum_values)
|
269
298
|
end
|
@@ -276,7 +305,8 @@ module Shoulda
|
|
276
305
|
if model.defined_enums.include?(attribute_name.to_s)
|
277
306
|
true
|
278
307
|
else
|
279
|
-
@
|
308
|
+
@failure_message_continuation =
|
309
|
+
"no such enum exists on #{model}"
|
280
310
|
false
|
281
311
|
end
|
282
312
|
end
|
@@ -289,11 +319,9 @@ module Shoulda
|
|
289
319
|
if passed
|
290
320
|
true
|
291
321
|
else
|
292
|
-
@
|
293
|
-
"
|
294
|
-
|
295
|
-
presented_actual_enum_values,
|
296
|
-
)
|
322
|
+
@failure_message_continuation =
|
323
|
+
"However, #{attribute_name.inspect} actually maps " +
|
324
|
+
presented_enum_mapping(normalized_actual_enum_values)
|
297
325
|
false
|
298
326
|
end
|
299
327
|
end
|
@@ -302,8 +330,8 @@ module Shoulda
|
|
302
330
|
if column.type == expected_column_type.to_sym
|
303
331
|
true
|
304
332
|
else
|
305
|
-
@
|
306
|
-
"#{attribute_name.inspect} is " +
|
333
|
+
@failure_message_continuation =
|
334
|
+
"However, #{attribute_name.inspect} is " +
|
307
335
|
Shoulda::Matchers::Util.a_or_an(column.type) +
|
308
336
|
' column'
|
309
337
|
false
|
@@ -330,30 +358,59 @@ module Shoulda
|
|
330
358
|
if passed
|
331
359
|
true
|
332
360
|
else
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
361
|
+
message = "#{attribute_name.inspect} does map to these "
|
362
|
+
message << 'values, but the enum is '
|
363
|
+
|
364
|
+
if expected_prefix
|
365
|
+
if expected_suffix
|
366
|
+
message << 'configured with either a different prefix or '
|
367
|
+
message << 'suffix, or no prefix or suffix at all'
|
368
|
+
else
|
369
|
+
message << 'configured with either a different prefix or no '
|
370
|
+
message << 'prefix at all'
|
343
371
|
end
|
372
|
+
elsif expected_suffix
|
373
|
+
message << 'configured with either a different suffix or no '
|
374
|
+
message << 'suffix at all'
|
375
|
+
end
|
376
|
+
|
377
|
+
message << " (we can't tell which)"
|
378
|
+
|
379
|
+
@failure_message_continuation = message
|
380
|
+
|
344
381
|
false
|
345
382
|
end
|
346
383
|
end
|
347
384
|
|
348
385
|
def expected_singleton_methods
|
349
386
|
expected_enum_value_names.map do |name|
|
350
|
-
[
|
387
|
+
[expected_prefix, name, expected_suffix].
|
351
388
|
select(&:present?).
|
352
389
|
join('_').
|
353
390
|
to_sym
|
354
391
|
end
|
355
392
|
end
|
356
393
|
|
394
|
+
def expected_prefix
|
395
|
+
if options.include?(:prefix)
|
396
|
+
if options[:prefix] == true
|
397
|
+
attribute_name#.to_sym
|
398
|
+
else
|
399
|
+
options[:prefix]#.to_sym
|
400
|
+
end
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
def expected_suffix
|
405
|
+
if options.include?(:suffix)
|
406
|
+
if options[:suffix] == true
|
407
|
+
attribute_name#.to_sym
|
408
|
+
else
|
409
|
+
options[:suffix]#.to_sym
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
357
414
|
def to_hash(value)
|
358
415
|
if value.is_a?(Array)
|
359
416
|
value.each_with_index.inject({}) do |hash, (item, index)|
|
@@ -2,7 +2,9 @@ module Shoulda
|
|
2
2
|
module Matchers
|
3
3
|
module ActiveRecord
|
4
4
|
# The `have_db_index` matcher tests that the table that backs your model
|
5
|
-
# has a
|
5
|
+
# has a specific index.
|
6
|
+
#
|
7
|
+
# You can specify one column:
|
6
8
|
#
|
7
9
|
# class CreateBlogs < ActiveRecord::Migration
|
8
10
|
# def change
|
@@ -24,43 +26,83 @@ module Shoulda
|
|
24
26
|
# should have_db_index(:user_id)
|
25
27
|
# end
|
26
28
|
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
# ##### unique
|
30
|
-
#
|
31
|
-
# Use `unique` to assert that the index is unique.
|
29
|
+
# Or you can specify a group of columns:
|
32
30
|
#
|
33
31
|
# class CreateBlogs < ActiveRecord::Migration
|
34
32
|
# def change
|
35
33
|
# create_table :blogs do |t|
|
34
|
+
# t.integer :user_id
|
36
35
|
# t.string :name
|
37
36
|
# end
|
38
37
|
#
|
39
|
-
# add_index :blogs, :
|
38
|
+
# add_index :blogs, :user_id, :name
|
40
39
|
# end
|
41
40
|
# end
|
42
41
|
#
|
43
42
|
# # RSpec
|
44
43
|
# RSpec.describe Blog, type: :model do
|
45
|
-
# it { should have_db_index(:name)
|
44
|
+
# it { should have_db_index([:user_id, :name]) }
|
46
45
|
# end
|
47
46
|
#
|
48
47
|
# # Minitest (Shoulda)
|
49
48
|
# class BlogTest < ActiveSupport::TestCase
|
50
|
-
# should have_db_index(:name)
|
49
|
+
# should have_db_index([:user_id, :name])
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# Finally, if you're using Rails 5 and PostgreSQL, you can also specify an
|
53
|
+
# expression:
|
54
|
+
#
|
55
|
+
# class CreateLoggedErrors < ActiveRecord::Migration
|
56
|
+
# def change
|
57
|
+
# create_table :logged_errors do |t|
|
58
|
+
# t.string :code
|
59
|
+
# t.jsonb :content
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# add_index :logged_errors, 'lower(code)::text'
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# # RSpec
|
67
|
+
# RSpec.describe LoggedError, type: :model do
|
68
|
+
# it { should have_db_index('lower(code)::text') }
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# # Minitest (Shoulda)
|
72
|
+
# class LoggedErrorTest < ActiveSupport::TestCase
|
73
|
+
# should have_db_index('lower(code)::text')
|
51
74
|
# end
|
52
75
|
#
|
53
|
-
#
|
54
|
-
#
|
76
|
+
# #### Qualifiers
|
77
|
+
#
|
78
|
+
# ##### unique
|
79
|
+
#
|
80
|
+
# Use `unique` to assert that the index is either unique or non-unique:
|
81
|
+
#
|
82
|
+
# class CreateBlogs < ActiveRecord::Migration
|
83
|
+
# def change
|
84
|
+
# create_table :blogs do |t|
|
85
|
+
# t.string :domain
|
86
|
+
# t.integer :user_id
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
# add_index :blogs, :domain, unique: true
|
90
|
+
# add_index :blogs, :user_id
|
91
|
+
# end
|
92
|
+
# end
|
55
93
|
#
|
56
94
|
# # RSpec
|
57
95
|
# RSpec.describe Blog, type: :model do
|
58
96
|
# it { should have_db_index(:name).unique }
|
97
|
+
# it { should have_db_index(:name).unique(true) } # if you want to be explicit
|
98
|
+
# it { should have_db_index(:user_id).unique(false) }
|
59
99
|
# end
|
60
100
|
#
|
61
101
|
# # Minitest (Shoulda)
|
62
102
|
# class BlogTest < ActiveSupport::TestCase
|
63
103
|
# should have_db_index(:name).unique
|
104
|
+
# should have_db_index(:name).unique(true) # if you want to be explicit
|
105
|
+
# should have_db_index(:user_id).unique(false)
|
64
106
|
# end
|
65
107
|
#
|
66
108
|
# @return [HaveDbIndexMatcher]
|
@@ -72,12 +114,12 @@ module Shoulda
|
|
72
114
|
# @private
|
73
115
|
class HaveDbIndexMatcher
|
74
116
|
def initialize(columns)
|
75
|
-
@
|
76
|
-
@
|
117
|
+
@expected_columns = normalize_columns_to_array(columns)
|
118
|
+
@qualifiers = {}
|
77
119
|
end
|
78
120
|
|
79
121
|
def unique(unique = true)
|
80
|
-
@
|
122
|
+
@qualifiers[:unique] = unique
|
81
123
|
self
|
82
124
|
end
|
83
125
|
|
@@ -87,72 +129,146 @@ module Shoulda
|
|
87
129
|
end
|
88
130
|
|
89
131
|
def failure_message
|
90
|
-
|
132
|
+
message =
|
133
|
+
"Expected #{described_table_name} to #{positive_expectation}"
|
134
|
+
|
135
|
+
message <<
|
136
|
+
if index_exists?
|
137
|
+
". The index does exist, but #{reason}."
|
138
|
+
elsif reason
|
139
|
+
", but #{reason}."
|
140
|
+
else
|
141
|
+
', but it does not.'
|
142
|
+
end
|
143
|
+
|
144
|
+
Shoulda::Matchers.word_wrap(message)
|
91
145
|
end
|
92
146
|
|
93
147
|
def failure_message_when_negated
|
94
|
-
|
148
|
+
Shoulda::Matchers.word_wrap(
|
149
|
+
"Expected #{described_table_name} not to " +
|
150
|
+
"#{negative_expectation}, but it does.",
|
151
|
+
)
|
95
152
|
end
|
96
153
|
|
97
154
|
def description
|
98
|
-
|
99
|
-
"have a #{index_type} index on columns #{@columns.join(' and ')}"
|
100
|
-
else
|
101
|
-
"have an index on columns #{@columns.join(' and ')}"
|
102
|
-
end
|
103
|
-
end
|
155
|
+
description = 'have '
|
104
156
|
|
105
|
-
|
157
|
+
description <<
|
158
|
+
if qualifiers.include?(:unique)
|
159
|
+
Shoulda::Matchers::Util.a_or_an(index_type) + ' '
|
160
|
+
else
|
161
|
+
'an '
|
162
|
+
end
|
106
163
|
|
107
|
-
|
108
|
-
|
164
|
+
description << 'index on '
|
165
|
+
|
166
|
+
description << inspected_expected_columns
|
109
167
|
end
|
110
168
|
|
111
|
-
|
112
|
-
return true unless @options.key?(:unique)
|
169
|
+
private
|
113
170
|
|
114
|
-
|
171
|
+
attr_reader :expected_columns, :qualifiers, :subject, :reason
|
115
172
|
|
116
|
-
|
173
|
+
def normalize_columns_to_array(columns)
|
174
|
+
Array.wrap(columns).map(&:to_s)
|
175
|
+
end
|
117
176
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
end
|
177
|
+
def index_exists?
|
178
|
+
!matched_index.nil?
|
179
|
+
end
|
122
180
|
|
123
|
-
|
181
|
+
def correct_unique?
|
182
|
+
if qualifiers.include?(:unique)
|
183
|
+
if qualifiers[:unique] && !matched_index.unique
|
184
|
+
@reason = 'it is not unique'
|
185
|
+
false
|
186
|
+
elsif !qualifiers[:unique] && matched_index.unique
|
187
|
+
@reason = 'it is unique'
|
188
|
+
false
|
189
|
+
else
|
190
|
+
true
|
191
|
+
end
|
192
|
+
else
|
193
|
+
true
|
194
|
+
end
|
124
195
|
end
|
125
196
|
|
126
197
|
def matched_index
|
127
|
-
|
198
|
+
@_matched_index ||=
|
199
|
+
if expected_columns.one?
|
200
|
+
actual_indexes.detect do |index|
|
201
|
+
Array.wrap(index.columns) == expected_columns
|
202
|
+
end
|
203
|
+
else
|
204
|
+
actual_indexes.detect do |index|
|
205
|
+
index.columns == expected_columns
|
206
|
+
end
|
207
|
+
end
|
128
208
|
end
|
129
209
|
|
130
|
-
def
|
131
|
-
|
210
|
+
def actual_indexes
|
211
|
+
model.connection.indexes(table_name)
|
212
|
+
end
|
213
|
+
|
214
|
+
def described_table_name
|
215
|
+
if model
|
216
|
+
"the #{table_name} table"
|
217
|
+
else
|
218
|
+
'a table'
|
219
|
+
end
|
132
220
|
end
|
133
221
|
|
134
222
|
def table_name
|
135
|
-
|
223
|
+
model.table_name
|
224
|
+
end
|
225
|
+
|
226
|
+
def positive_expectation
|
227
|
+
if index_exists?
|
228
|
+
expectation = "have an index on #{inspected_expected_columns}"
|
229
|
+
|
230
|
+
if qualifiers.include?(:unique)
|
231
|
+
expectation << " and for it to be #{index_type}"
|
232
|
+
end
|
233
|
+
|
234
|
+
expectation
|
235
|
+
else
|
236
|
+
description
|
237
|
+
end
|
136
238
|
end
|
137
239
|
|
138
|
-
def
|
139
|
-
|
240
|
+
def negative_expectation
|
241
|
+
description
|
140
242
|
end
|
141
243
|
|
142
|
-
def
|
143
|
-
|
244
|
+
def inspected_expected_columns
|
245
|
+
if formatted_expected_columns.one?
|
246
|
+
formatted_expected_columns.first.inspect
|
247
|
+
else
|
248
|
+
formatted_expected_columns.inspect
|
249
|
+
end
|
144
250
|
end
|
145
251
|
|
146
252
|
def index_type
|
147
|
-
if
|
253
|
+
if qualifiers[:unique]
|
148
254
|
'unique'
|
149
255
|
else
|
150
256
|
'non-unique'
|
151
257
|
end
|
152
258
|
end
|
153
259
|
|
154
|
-
def
|
155
|
-
|
260
|
+
def formatted_expected_columns
|
261
|
+
expected_columns.map do |column|
|
262
|
+
if column.match?(/^\w+$/)
|
263
|
+
column.to_sym
|
264
|
+
else
|
265
|
+
column
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def model
|
271
|
+
subject&.class
|
156
272
|
end
|
157
273
|
end
|
158
274
|
end
|