shoulda-matchers 4.0.1 → 4.1.0

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