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.
@@ -272,7 +272,7 @@ module Shoulda
272
272
  # should belong_to(:organization).required
273
273
  # end
274
274
  #
275
- # #### without_validating_presence
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
- if klass.column_names.include?(foreign_key)
1367
- true
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 = "define :#{attribute_name} as an enum, backed by "
147
+ description = "#{simple_description} backed by "
148
148
  description << Shoulda::Matchers::Util.a_or_an(expected_column_type)
149
149
 
150
- if options[:expected_prefix]
151
- description << ', using a prefix of '
152
- description << "#{options[:expected_prefix].inspect}"
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[:expected_suffix]
156
- if options[:expected_prefix]
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 presented_expected_enum_values.any?
168
- description << ', with possible values '
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 = attribute_name)
191
- options[:expected_prefix] = expected_prefix
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 = attribute_name)
196
- options[:expected_suffix] = expected_suffix
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 = "Expected #{model} to #{expectation}"
216
-
217
- if failure_reason
218
- message << ". However, #{failure_reason}"
219
- end
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, :failure_reason
225
+ attr_reader :attribute_name, :options, :record,
226
+ :failure_message_continuation
234
227
 
235
228
  def expectation
236
- description
237
- end
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
- def presented_expected_enum_values
240
- if expected_enum_values.is_a?(Hash)
241
- expected_enum_values.symbolize_keys
264
+ expectation
242
265
  else
243
- expected_enum_values
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
- @failure_reason = "no such enum exists in #{model}"
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
- @failure_reason =
293
- "the actual enum values for #{attribute_name.inspect} are " +
294
- Shoulda::Matchers::Util.inspect_value(
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
- @failure_reason =
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
- @failure_reason =
334
- if options[:expected_prefix]
335
- if options[:expected_suffix]
336
- 'it was defined with either a different prefix, a ' +
337
- 'different suffix, or neither one at all'
338
- else
339
- 'it was defined with either a different prefix or none at all'
340
- end
341
- elsif options[:expected_suffix]
342
- 'it was defined with either a different suffix or none at all'
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
- [options[:expected_prefix], name, options[:expected_suffix]].
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 index on a specific column.
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
- # #### Qualifiers
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, :name, unique: true
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).unique(true) }
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).unique(true)
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
- # Since it only ever makes sense for `unique` to be `true`, you can also
54
- # leave off the argument to save some keystrokes:
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
- @columns = normalize_columns_to_array(columns)
76
- @options = {}
117
+ @expected_columns = normalize_columns_to_array(columns)
118
+ @qualifiers = {}
77
119
  end
78
120
 
79
121
  def unique(unique = true)
80
- @options[:unique] = unique
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
- "Expected #{expectation} (#{@missing})"
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
- "Did not expect #{expectation}"
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
- if @options.key?(:unique)
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
- protected
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
- def index_exists?
108
- ! matched_index.nil?
164
+ description << 'index on '
165
+
166
+ description << inspected_expected_columns
109
167
  end
110
168
 
111
- def correct_unique?
112
- return true unless @options.key?(:unique)
169
+ private
113
170
 
114
- is_unique = matched_index.unique
171
+ attr_reader :expected_columns, :qualifiers, :subject, :reason
115
172
 
116
- is_unique = !is_unique unless @options[:unique]
173
+ def normalize_columns_to_array(columns)
174
+ Array.wrap(columns).map(&:to_s)
175
+ end
117
176
 
118
- unless is_unique
119
- @missing = "#{table_name} has an index named #{matched_index.name} " <<
120
- "of unique #{matched_index.unique}, not #{@options[:unique]}."
121
- end
177
+ def index_exists?
178
+ !matched_index.nil?
179
+ end
122
180
 
123
- is_unique
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
- indexes.detect { |each| each.columns == @columns }
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 model_class
131
- @subject.class
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
- model_class.table_name
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 indexes
139
- ::ActiveRecord::Base.connection.indexes(table_name)
240
+ def negative_expectation
241
+ description
140
242
  end
141
243
 
142
- def expectation
143
- "#{model_class.name} to #{description}"
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 @options[:unique]
253
+ if qualifiers[:unique]
148
254
  'unique'
149
255
  else
150
256
  'non-unique'
151
257
  end
152
258
  end
153
259
 
154
- def normalize_columns_to_array(columns)
155
- Array.wrap(columns).map(&:to_s)
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