shoulda-matchers 3.0.1 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +3 -3
- data/CONTRIBUTING.md +60 -28
- data/Gemfile +1 -0
- data/Gemfile.lock +15 -12
- data/NEWS.md +111 -0
- data/README.md +94 -6
- data/Rakefile +10 -8
- data/custom_plan.rb +88 -0
- data/gemfiles/4.0.0.gemfile +1 -0
- data/gemfiles/4.0.0.gemfile.lock +21 -18
- data/gemfiles/4.0.1.gemfile +1 -0
- data/gemfiles/4.0.1.gemfile.lock +21 -18
- data/gemfiles/4.1.gemfile +1 -0
- data/gemfiles/4.1.gemfile.lock +21 -18
- data/gemfiles/4.2.gemfile +1 -0
- data/gemfiles/4.2.gemfile.lock +24 -21
- data/lib/shoulda/matchers/action_controller/permit_matcher.rb +6 -11
- data/lib/shoulda/matchers/active_model.rb +10 -1
- data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +258 -180
- data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_changed_value_error.rb +45 -0
- data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_does_not_exist_error.rb +23 -0
- data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setter.rb +236 -0
- data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setter_and_validator.rb +62 -0
- data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters.rb +40 -0
- data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters_and_validators.rb +48 -0
- data/lib/shoulda/matchers/active_model/allow_value_matcher/successful_check.rb +14 -0
- data/lib/shoulda/matchers/active_model/allow_value_matcher/successful_setting.rb +14 -0
- data/lib/shoulda/matchers/active_model/disallow_value_matcher.rb +34 -14
- data/lib/shoulda/matchers/active_model/helpers.rb +9 -17
- data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +13 -6
- data/lib/shoulda/matchers/active_model/numericality_matchers/even_number_matcher.rb +13 -2
- data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +19 -35
- data/lib/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher.rb +13 -2
- data/lib/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher.rb +12 -2
- data/lib/shoulda/matchers/active_model/qualifiers.rb +12 -0
- data/lib/shoulda/matchers/active_model/qualifiers/ignore_interference_by_writer.rb +101 -0
- data/lib/shoulda/matchers/active_model/qualifiers/ignoring_interference_by_writer.rb +21 -0
- data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +30 -32
- data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +5 -8
- data/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +22 -22
- data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +27 -16
- data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +58 -15
- data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +22 -12
- data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +165 -87
- data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +7 -9
- data/lib/shoulda/matchers/active_model/validation_matcher.rb +111 -49
- data/lib/shoulda/matchers/active_model/validation_matcher/build_description.rb +60 -0
- data/lib/shoulda/matchers/active_model/validator.rb +71 -52
- data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +19 -5
- data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +450 -124
- data/lib/shoulda/matchers/util.rb +43 -0
- data/lib/shoulda/matchers/util/word_wrap.rb +59 -31
- data/lib/shoulda/matchers/version.rb +1 -1
- data/script/update_gem_in_all_appraisals +1 -1
- data/script/update_gems_in_all_appraisals +1 -1
- data/spec/acceptance/multiple_libraries_integration_spec.rb +5 -2
- data/spec/acceptance/rails_integration_spec.rb +6 -2
- data/spec/spec_helper.rb +1 -3
- data/spec/support/acceptance/helpers/step_helpers.rb +4 -1
- data/spec/support/tests/current_bundle.rb +21 -7
- data/spec/support/unit/active_record/create_table.rb +54 -0
- data/spec/support/unit/attribute.rb +47 -0
- data/spec/support/unit/capture.rb +6 -0
- data/spec/support/unit/change_value.rb +111 -0
- data/spec/support/unit/create_model_arguments/basic.rb +135 -0
- data/spec/support/unit/create_model_arguments/has_many.rb +15 -0
- data/spec/support/unit/create_model_arguments/uniqueness_matcher.rb +74 -0
- data/spec/support/unit/helpers/active_record_versions.rb +1 -1
- data/spec/support/unit/helpers/class_builder.rb +61 -47
- data/spec/support/unit/helpers/database_helpers.rb +5 -3
- data/spec/support/unit/helpers/model_builder.rb +77 -97
- data/spec/support/unit/helpers/validation_matcher_scenario_helpers.rb +44 -0
- data/spec/support/unit/load_environment.rb +12 -0
- data/spec/support/unit/matchers/fail_with_message_including_matcher.rb +2 -2
- data/spec/support/unit/matchers/fail_with_message_matcher.rb +12 -1
- data/spec/support/unit/model_creation_strategies/active_model.rb +111 -0
- data/spec/support/unit/model_creation_strategies/active_record.rb +77 -0
- data/spec/support/unit/model_creators.rb +19 -0
- data/spec/support/unit/model_creators/active_model.rb +39 -0
- data/spec/support/unit/model_creators/active_record.rb +43 -0
- data/spec/support/unit/model_creators/active_record/has_and_belongs_to_many.rb +95 -0
- data/spec/support/unit/model_creators/active_record/has_many.rb +67 -0
- data/spec/support/unit/model_creators/active_record/uniqueness_matcher.rb +42 -0
- data/spec/support/unit/model_creators/basic.rb +97 -0
- data/spec/support/unit/rails_application.rb +1 -1
- data/spec/support/unit/record_validating_confirmation_builder.rb +3 -7
- data/spec/support/unit/shared_examples/ignoring_interference_by_writer.rb +79 -0
- data/spec/support/unit/validation_matcher_scenario.rb +62 -0
- data/spec/unit/shoulda/matchers/active_model/allow_mass_assignment_of_matcher_spec.rb +4 -0
- data/spec/unit/shoulda/matchers/active_model/allow_value_matcher_spec.rb +575 -140
- data/spec/unit/shoulda/matchers/active_model/validate_absence_of_matcher_spec.rb +115 -15
- data/spec/unit/shoulda/matchers/active_model/validate_acceptance_of_matcher_spec.rb +42 -4
- data/spec/unit/shoulda/matchers/active_model/validate_confirmation_of_matcher_spec.rb +92 -6
- data/spec/unit/shoulda/matchers/active_model/validate_exclusion_of_matcher_spec.rb +122 -10
- data/spec/unit/shoulda/matchers/active_model/validate_inclusion_of_matcher_spec.rb +306 -58
- data/spec/unit/shoulda/matchers/active_model/validate_length_of_matcher_spec.rb +122 -3
- data/spec/unit/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +805 -131
- data/spec/unit/shoulda/matchers/active_model/validate_presence_of_matcher_spec.rb +196 -29
- data/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb +82 -40
- data/spec/unit/shoulda/matchers/active_record/validate_uniqueness_of_matcher_spec.rb +600 -101
- data/spec/unit/shoulda/matchers/util/word_wrap_spec.rb +88 -33
- data/spec/unit_spec_helper.rb +10 -22
- data/zeus.json +11 -0
- metadata +64 -23
- data/lib/shoulda/matchers/active_model/strict_validator.rb +0 -51
- data/spec/support/unit/shared_examples/numerical_type_submatcher.rb +0 -15
- data/spec/unit/shoulda/matchers/active_model/numericality_matchers/comparison_matcher_spec.rb +0 -288
- data/spec/unit/shoulda/matchers/active_model/numericality_matchers/even_number_matcher_spec.rb +0 -100
- data/spec/unit/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher_spec.rb +0 -100
- data/spec/unit/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher_spec.rb +0 -100
@@ -57,7 +57,7 @@ module Shoulda
|
|
57
57
|
#
|
58
58
|
# Failures:
|
59
59
|
#
|
60
|
-
# 1) Post should
|
60
|
+
# 1) Post should validate :title to be case-sensitively unique
|
61
61
|
# Failure/Error: it { should validate_uniqueness_of(:title) }
|
62
62
|
# ActiveRecord::StatementInvalid:
|
63
63
|
# SQLite3::ConstraintException: posts.content may not be NULL: INSERT INTO "posts" ("title") VALUES (?)
|
@@ -133,7 +133,7 @@ module Shoulda
|
|
133
133
|
# unique, but the scoped attributes are not unique either.
|
134
134
|
#
|
135
135
|
# class Post < ActiveRecord::Base
|
136
|
-
# validates_uniqueness_of :slug, scope: :
|
136
|
+
# validates_uniqueness_of :slug, scope: :journal_id
|
137
137
|
# end
|
138
138
|
#
|
139
139
|
# # RSpec
|
@@ -167,6 +167,40 @@ module Shoulda
|
|
167
167
|
# should validate_uniqueness_of(:key).case_insensitive
|
168
168
|
# end
|
169
169
|
#
|
170
|
+
# ##### ignoring_case_sensitivity
|
171
|
+
#
|
172
|
+
# By default, `validate_uniqueness_of` will check that the
|
173
|
+
# validation is case sensitive: it asserts that uniquable attributes pass
|
174
|
+
# validation when their values are in a different case than corresponding
|
175
|
+
# attributes in the pre-existing record.
|
176
|
+
#
|
177
|
+
# Use `ignoring_case_sensitivity` to skip this check. This qualifier is
|
178
|
+
# particularly handy if your model has somehow changed the behavior of
|
179
|
+
# attribute you're testing so that it modifies the case of incoming values
|
180
|
+
# as they are set. For instance, perhaps you've overridden the writer
|
181
|
+
# method or added a `before_validation` callback to normalize the
|
182
|
+
# attribute.
|
183
|
+
#
|
184
|
+
# class User < ActiveRecord::Base
|
185
|
+
# validates_uniqueness_of :email
|
186
|
+
#
|
187
|
+
# def email=(value)
|
188
|
+
# super(value.downcase)
|
189
|
+
# end
|
190
|
+
# end
|
191
|
+
#
|
192
|
+
# # RSpec
|
193
|
+
# describe Post do
|
194
|
+
# it do
|
195
|
+
# should validate_uniqueness_of(:email).ignoring_case_sensitivity
|
196
|
+
# end
|
197
|
+
# end
|
198
|
+
#
|
199
|
+
# # Minitest (Shoulda)
|
200
|
+
# class PostTest < ActiveSupport::TestCase
|
201
|
+
# should validate_uniqueness_of(:email).ignoring_case_sensitivity
|
202
|
+
# end
|
203
|
+
#
|
170
204
|
# ##### allow_nil
|
171
205
|
#
|
172
206
|
# Use `allow_nil` to assert that the attribute allows nil.
|
@@ -217,7 +251,17 @@ module Shoulda
|
|
217
251
|
|
218
252
|
def initialize(attribute)
|
219
253
|
super(attribute)
|
220
|
-
@
|
254
|
+
@expected_message = :taken
|
255
|
+
@options = {
|
256
|
+
case_sensitivity_strategy: :sensitive
|
257
|
+
}
|
258
|
+
@existing_record_created = false
|
259
|
+
@failure_reason = nil
|
260
|
+
@failure_reason_when_negated = nil
|
261
|
+
@attribute_setters = {
|
262
|
+
existing_record: [],
|
263
|
+
new_record: []
|
264
|
+
}
|
221
265
|
end
|
222
266
|
|
223
267
|
def scoped_to(*scopes)
|
@@ -225,13 +269,13 @@ module Shoulda
|
|
225
269
|
self
|
226
270
|
end
|
227
271
|
|
228
|
-
def
|
229
|
-
@
|
272
|
+
def case_insensitive
|
273
|
+
@options[:case_sensitivity_strategy] = :insensitive
|
230
274
|
self
|
231
275
|
end
|
232
276
|
|
233
|
-
def
|
234
|
-
@options[:
|
277
|
+
def ignoring_case_sensitivity
|
278
|
+
@options[:case_sensitivity_strategy] = :ignore
|
235
279
|
self
|
236
280
|
end
|
237
281
|
|
@@ -240,28 +284,40 @@ module Shoulda
|
|
240
284
|
self
|
241
285
|
end
|
242
286
|
|
287
|
+
def expects_to_allow_nil?
|
288
|
+
@options[:allow_nil]
|
289
|
+
end
|
290
|
+
|
243
291
|
def allow_blank
|
244
292
|
@options[:allow_blank] = true
|
245
293
|
self
|
246
294
|
end
|
247
295
|
|
248
|
-
def
|
249
|
-
|
250
|
-
result << "case sensitive " unless @options[:case_insensitive]
|
251
|
-
result << "unique value for #{@attribute}"
|
252
|
-
result << " scoped to #{@options[:scopes].join(', ')}" if @options[:scopes].present?
|
253
|
-
result
|
296
|
+
def expects_to_allow_blank?
|
297
|
+
@options[:allow_blank]
|
254
298
|
end
|
255
299
|
|
256
|
-
def
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
@all_records = @subject.class.all
|
300
|
+
def simple_description
|
301
|
+
description = "validate that :#{@attribute} is"
|
302
|
+
description << description_for_case_sensitive_qualifier
|
303
|
+
description << ' unique'
|
261
304
|
|
262
|
-
|
263
|
-
|
264
|
-
|
305
|
+
if @options[:scopes].present?
|
306
|
+
description << " within the scope of #{inspected_expected_scopes}"
|
307
|
+
end
|
308
|
+
|
309
|
+
description
|
310
|
+
end
|
311
|
+
|
312
|
+
def matches?(given_record)
|
313
|
+
@given_record = given_record
|
314
|
+
@all_records = model.all
|
315
|
+
|
316
|
+
existing_record_valid? &&
|
317
|
+
validate_attribute_present? &&
|
318
|
+
validate_scopes_present? &&
|
319
|
+
scopes_match? &&
|
320
|
+
validate_two_records_with_same_non_blank_value_cannot_coexist? &&
|
265
321
|
validate_case_sensitivity? &&
|
266
322
|
validate_after_scope_change? &&
|
267
323
|
allows_nil? &&
|
@@ -270,42 +326,102 @@ module Shoulda
|
|
270
326
|
Uniqueness::TestModels.remove_all
|
271
327
|
end
|
272
328
|
|
273
|
-
|
329
|
+
protected
|
274
330
|
|
275
|
-
def
|
276
|
-
@
|
277
|
-
|
331
|
+
def failure_reason
|
332
|
+
@failure_reason || super
|
333
|
+
end
|
334
|
+
|
335
|
+
def failure_reason_when_negated
|
336
|
+
@failure_reason_when_negated || super
|
337
|
+
end
|
338
|
+
|
339
|
+
def build_allow_or_disallow_value_matcher(args)
|
340
|
+
super.tap do |matcher|
|
341
|
+
matcher.failure_message_preface = method(:failure_message_preface)
|
342
|
+
matcher.attribute_changed_value_message =
|
343
|
+
method(:attribute_changed_value_message)
|
278
344
|
end
|
279
345
|
end
|
280
346
|
|
281
|
-
|
282
|
-
expected_scopes = Array.wrap(@options[:scopes])
|
347
|
+
private
|
283
348
|
|
284
|
-
|
285
|
-
|
349
|
+
def case_sensitivity_strategy
|
350
|
+
@options[:case_sensitivity_strategy]
|
351
|
+
end
|
352
|
+
|
353
|
+
def new_record
|
354
|
+
unless defined?(@new_record)
|
355
|
+
build_new_record
|
356
|
+
end
|
357
|
+
|
358
|
+
@new_record
|
359
|
+
end
|
360
|
+
alias_method :subject, :new_record
|
361
|
+
|
362
|
+
def description_for_case_sensitive_qualifier
|
363
|
+
case case_sensitivity_strategy
|
364
|
+
when :sensitive
|
365
|
+
' case-sensitively'
|
366
|
+
when :insensitive
|
367
|
+
' case-insensitively'
|
286
368
|
else
|
287
|
-
|
369
|
+
''
|
288
370
|
end
|
371
|
+
end
|
372
|
+
|
373
|
+
def validation
|
374
|
+
model._validators[@attribute].detect do |validator|
|
375
|
+
validator.is_a?(::ActiveRecord::Validations::UniquenessValidator)
|
376
|
+
end
|
377
|
+
end
|
289
378
|
|
379
|
+
def scopes_match?
|
290
380
|
if expected_scopes == actual_scopes
|
291
381
|
true
|
292
382
|
else
|
293
|
-
@
|
294
|
-
"#{expected_scopes}"
|
383
|
+
@failure_reason = 'Expected the validation'
|
295
384
|
|
296
|
-
if
|
297
|
-
@
|
385
|
+
if expected_scopes.empty?
|
386
|
+
@failure_reason << ' not to be scoped to anything'
|
298
387
|
else
|
299
|
-
@
|
388
|
+
@failure_reason << " to be scoped to #{inspected_expected_scopes}"
|
389
|
+
end
|
390
|
+
|
391
|
+
if actual_scopes.empty?
|
392
|
+
@failure_reason << ', but it was not scoped to anything.'
|
393
|
+
else
|
394
|
+
@failure_reason << ', but it was scoped to '
|
395
|
+
@failure_reason << "#{inspected_actual_scopes} instead."
|
300
396
|
end
|
301
397
|
|
302
398
|
false
|
303
399
|
end
|
304
400
|
end
|
305
401
|
|
402
|
+
def expected_scopes
|
403
|
+
Array.wrap(@options[:scopes])
|
404
|
+
end
|
405
|
+
|
406
|
+
def inspected_expected_scopes
|
407
|
+
expected_scopes.map(&:inspect).to_sentence
|
408
|
+
end
|
409
|
+
|
410
|
+
def actual_scopes
|
411
|
+
if validation
|
412
|
+
Array.wrap(validation.options[:scope])
|
413
|
+
else
|
414
|
+
[]
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
def inspected_actual_scopes
|
419
|
+
actual_scopes.map(&:inspect).to_sentence
|
420
|
+
end
|
421
|
+
|
306
422
|
def allows_nil?
|
307
|
-
if
|
308
|
-
|
423
|
+
if expects_to_allow_nil?
|
424
|
+
update_existing_record!(nil)
|
309
425
|
allows_value_of(nil, @expected_message)
|
310
426
|
else
|
311
427
|
true
|
@@ -313,48 +429,69 @@ module Shoulda
|
|
313
429
|
end
|
314
430
|
|
315
431
|
def allows_blank?
|
316
|
-
if
|
317
|
-
|
432
|
+
if expects_to_allow_blank?
|
433
|
+
update_existing_record!('')
|
318
434
|
allows_value_of('', @expected_message)
|
319
435
|
else
|
320
436
|
true
|
321
437
|
end
|
322
438
|
end
|
323
439
|
|
324
|
-
def
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
440
|
+
def existing_record_valid?
|
441
|
+
if existing_record.valid?
|
442
|
+
true
|
443
|
+
else
|
444
|
+
@failure_reason =
|
445
|
+
"The record you provided could not be created, " +
|
446
|
+
"as it failed with the following validation errors:\n\n" +
|
447
|
+
format_validation_errors(existing_record.errors)
|
448
|
+
false
|
449
|
+
end
|
330
450
|
end
|
331
451
|
|
332
|
-
def
|
333
|
-
unless
|
334
|
-
|
452
|
+
def existing_record
|
453
|
+
unless defined?(@existing_record)
|
454
|
+
find_or_create_existing_record
|
335
455
|
end
|
456
|
+
|
457
|
+
@existing_record
|
336
458
|
end
|
337
459
|
|
338
|
-
def
|
339
|
-
|
340
|
-
|
460
|
+
def find_or_create_existing_record
|
461
|
+
@existing_record = find_existing_record
|
462
|
+
|
463
|
+
unless @existing_record
|
464
|
+
@existing_record = create_existing_record
|
465
|
+
@existing_record_created = true
|
341
466
|
end
|
342
467
|
end
|
343
468
|
|
344
|
-
def
|
345
|
-
|
469
|
+
def find_existing_record
|
470
|
+
record = model.first
|
471
|
+
|
472
|
+
if record.present?
|
473
|
+
record
|
474
|
+
else
|
475
|
+
nil
|
476
|
+
end
|
346
477
|
end
|
347
478
|
|
348
|
-
def
|
349
|
-
@
|
479
|
+
def create_existing_record
|
480
|
+
@given_record.tap do |existing_record|
|
481
|
+
ensure_secure_password_set(existing_record)
|
482
|
+
existing_record.save
|
483
|
+
end
|
350
484
|
end
|
351
485
|
|
352
|
-
def
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
486
|
+
def update_existing_record!(value)
|
487
|
+
if existing_value_read != value
|
488
|
+
set_attribute_on!(
|
489
|
+
:existing_record,
|
490
|
+
existing_record,
|
491
|
+
@attribute,
|
492
|
+
value
|
493
|
+
)
|
494
|
+
existing_record.save!
|
358
495
|
end
|
359
496
|
end
|
360
497
|
|
@@ -365,70 +502,117 @@ module Shoulda
|
|
365
502
|
end
|
366
503
|
end
|
367
504
|
|
368
|
-
def
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
505
|
+
def arbitrary_non_blank_value
|
506
|
+
limit = column_limit_for(@attribute)
|
507
|
+
non_blank_value = 'an arbitrary value'
|
508
|
+
|
509
|
+
if limit && limit < non_blank_value.length
|
510
|
+
'x' * limit
|
511
|
+
else
|
512
|
+
non_blank_value
|
373
513
|
end
|
374
514
|
end
|
375
515
|
|
376
516
|
def has_secure_password?
|
377
|
-
|
517
|
+
model.ancestors.map(&:to_s).include?(
|
518
|
+
'ActiveModel::SecurePassword::InstanceMethodsOnActivation'
|
519
|
+
)
|
378
520
|
end
|
379
521
|
|
380
|
-
def
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
522
|
+
def build_new_record
|
523
|
+
@new_record = existing_record.dup
|
524
|
+
|
525
|
+
attribute_names_under_test.each do |attribute_name|
|
526
|
+
set_attribute_on_new_record!(
|
527
|
+
attribute_name,
|
528
|
+
existing_record.public_send(attribute_name)
|
529
|
+
)
|
530
|
+
end
|
531
|
+
|
532
|
+
@new_record
|
533
|
+
end
|
534
|
+
|
535
|
+
def validate_attribute_present?
|
536
|
+
if model.method_defined?("#{attribute}=")
|
537
|
+
true
|
392
538
|
else
|
539
|
+
@failure_reason =
|
540
|
+
":#{attribute} does not seem to be an attribute on #{model.name}."
|
541
|
+
false
|
542
|
+
end
|
543
|
+
end
|
544
|
+
|
545
|
+
def validate_scopes_present?
|
546
|
+
if all_scopes_present_on_model?
|
393
547
|
true
|
548
|
+
else
|
549
|
+
reason = ''
|
550
|
+
|
551
|
+
reason << inspected_scopes_missing_on_model.to_sentence
|
552
|
+
|
553
|
+
if inspected_scopes_missing_on_model.many?
|
554
|
+
reason << " do not seem to be attributes"
|
555
|
+
else
|
556
|
+
reason << " does not seem to be an attribute"
|
557
|
+
end
|
558
|
+
|
559
|
+
reason << " on #{model.name}."
|
560
|
+
|
561
|
+
@failure_reason = reason
|
562
|
+
|
563
|
+
false
|
394
564
|
end
|
395
565
|
end
|
396
566
|
|
397
|
-
def
|
398
|
-
|
399
|
-
|
400
|
-
|
567
|
+
def all_scopes_present_on_model?
|
568
|
+
scopes_missing_on_model.none?
|
569
|
+
end
|
570
|
+
|
571
|
+
def scopes_missing_on_model
|
572
|
+
@_missing_scopes ||= expected_scopes.select do |scope|
|
573
|
+
!model.method_defined?("#{scope}=")
|
401
574
|
end
|
575
|
+
end
|
402
576
|
|
403
|
-
|
577
|
+
def inspected_scopes_missing_on_model
|
578
|
+
scopes_missing_on_model.map(&:inspect)
|
404
579
|
end
|
405
580
|
|
406
|
-
def
|
407
|
-
|
581
|
+
def validate_two_records_with_same_non_blank_value_cannot_coexist?
|
582
|
+
if existing_value_read.blank?
|
583
|
+
update_existing_record!(arbitrary_non_blank_value)
|
584
|
+
end
|
585
|
+
|
586
|
+
disallows_value_of(existing_value_read, @expected_message)
|
587
|
+
end
|
408
588
|
|
409
|
-
|
589
|
+
def validate_case_sensitivity?
|
590
|
+
if should_validate_case_sensitivity?
|
591
|
+
value = existing_value_read
|
410
592
|
swapcased_value = value.swapcase
|
411
593
|
|
412
|
-
if
|
413
|
-
disallows_value_of(swapcased_value, @expected_message)
|
414
|
-
else
|
594
|
+
if case_sensitivity_strategy == :sensitive
|
415
595
|
if value == swapcased_value
|
416
596
|
raise NonCaseSwappableValueError.create(
|
417
|
-
model:
|
597
|
+
model: model,
|
418
598
|
attribute: @attribute,
|
419
599
|
value: value
|
420
600
|
)
|
421
601
|
end
|
422
602
|
|
423
603
|
allows_value_of(swapcased_value, @expected_message)
|
604
|
+
else
|
605
|
+
disallows_value_of(swapcased_value, @expected_message)
|
424
606
|
end
|
425
607
|
else
|
426
608
|
true
|
427
609
|
end
|
428
610
|
end
|
429
611
|
|
430
|
-
def
|
431
|
-
|
612
|
+
def should_validate_case_sensitivity?
|
613
|
+
case_sensitivity_strategy != :ignore &&
|
614
|
+
existing_value_read.respond_to?(:swapcase) &&
|
615
|
+
!existing_value_read.empty?
|
432
616
|
end
|
433
617
|
|
434
618
|
def model_class?(model_name)
|
@@ -438,10 +622,10 @@ module Shoulda
|
|
438
622
|
end
|
439
623
|
|
440
624
|
def validate_after_scope_change?
|
441
|
-
if
|
625
|
+
if expected_scopes.empty? || all_scopes_are_booleans?
|
442
626
|
true
|
443
627
|
else
|
444
|
-
|
628
|
+
expected_scopes.all? do |scope|
|
445
629
|
previous_value = @all_records.map(&scope).compact.max
|
446
630
|
|
447
631
|
next_value =
|
@@ -451,16 +635,12 @@ module Shoulda
|
|
451
635
|
next_value_for(scope, previous_value)
|
452
636
|
end
|
453
637
|
|
454
|
-
|
455
|
-
|
456
|
-
if allows_value_of(existing_value, @expected_message)
|
457
|
-
@subject.__send__("#{scope}=", previous_value)
|
638
|
+
set_attribute_on_new_record!(scope, next_value)
|
458
639
|
|
459
|
-
|
460
|
-
|
640
|
+
if allows_value_of(existing_value_read, @expected_message)
|
641
|
+
set_attribute_on_new_record!(scope, previous_value)
|
461
642
|
true
|
462
643
|
else
|
463
|
-
@failure_message << " (with different value of #{scope})"
|
464
644
|
false
|
465
645
|
end
|
466
646
|
end
|
@@ -478,20 +658,7 @@ module Shoulda
|
|
478
658
|
end
|
479
659
|
|
480
660
|
def dummy_scalar_value_for(column)
|
481
|
-
|
482
|
-
when :integer
|
483
|
-
0
|
484
|
-
when :date
|
485
|
-
Date.today
|
486
|
-
when :datetime
|
487
|
-
DateTime.now
|
488
|
-
when :uuid
|
489
|
-
SecureRandom.uuid
|
490
|
-
when :boolean
|
491
|
-
true
|
492
|
-
else
|
493
|
-
'dummy value'
|
494
|
-
end
|
661
|
+
Shoulda::Matchers::Util.dummy_value_for(column.type)
|
495
662
|
end
|
496
663
|
|
497
664
|
def next_value_for(scope, previous_value)
|
@@ -534,8 +701,8 @@ module Shoulda
|
|
534
701
|
end
|
535
702
|
|
536
703
|
def defined_as_enum?(scope)
|
537
|
-
|
538
|
-
|
704
|
+
model.respond_to?(:defined_enums) &&
|
705
|
+
new_record.defined_enums[scope.to_s]
|
539
706
|
end
|
540
707
|
|
541
708
|
def polymorphic_type_attribute?(scope, previous_value)
|
@@ -543,21 +710,180 @@ module Shoulda
|
|
543
710
|
end
|
544
711
|
|
545
712
|
def available_enum_values_for(scope, previous_value)
|
546
|
-
|
713
|
+
new_record.defined_enums[scope.to_s].reject do |key, _|
|
547
714
|
key == previous_value
|
548
715
|
end
|
549
716
|
end
|
550
717
|
|
551
|
-
def
|
552
|
-
|
718
|
+
def set_attribute_on!(record_type, record, attribute_name, value)
|
719
|
+
attribute_setter = build_attribute_setter(
|
720
|
+
record,
|
721
|
+
attribute_name,
|
722
|
+
value
|
723
|
+
)
|
724
|
+
attribute_setter.set!
|
725
|
+
|
726
|
+
@attribute_setters[record_type] << attribute_setter
|
727
|
+
end
|
728
|
+
|
729
|
+
def set_attribute_on_existing_record!(attribute_name, value)
|
730
|
+
set_attribute_on!(
|
731
|
+
:existing_record,
|
732
|
+
existing_record,
|
733
|
+
attribute_name,
|
734
|
+
value
|
735
|
+
)
|
736
|
+
end
|
737
|
+
|
738
|
+
def set_attribute_on_new_record!(attribute_name, value)
|
739
|
+
set_attribute_on!(
|
740
|
+
:new_record,
|
741
|
+
new_record,
|
742
|
+
attribute_name,
|
743
|
+
value
|
744
|
+
)
|
745
|
+
end
|
746
|
+
|
747
|
+
def attribute_setter_for_existing_record
|
748
|
+
@attribute_setters[:existing_record].last
|
749
|
+
end
|
750
|
+
|
751
|
+
def attribute_names_under_test
|
752
|
+
[@attribute] + expected_scopes
|
553
753
|
end
|
554
754
|
|
555
|
-
def
|
556
|
-
|
755
|
+
def build_attribute_setter(record, attribute_name, value)
|
756
|
+
Shoulda::Matchers::ActiveModel::AllowValueMatcher::AttributeSetter.new(
|
757
|
+
matcher_name: :validate_uniqueness_of,
|
758
|
+
object: record,
|
759
|
+
attribute_name: attribute_name,
|
760
|
+
value: value,
|
761
|
+
ignore_interference_by_writer: ignore_interference_by_writer
|
762
|
+
)
|
763
|
+
end
|
764
|
+
|
765
|
+
def existing_value_read
|
766
|
+
existing_record.public_send(@attribute)
|
767
|
+
end
|
768
|
+
|
769
|
+
def existing_value_written
|
770
|
+
if attribute_setter_for_existing_record
|
771
|
+
attribute_setter_for_existing_record.value_written
|
772
|
+
else
|
773
|
+
existing_value_read
|
774
|
+
end
|
557
775
|
end
|
558
776
|
|
559
777
|
def column_for(scope)
|
560
|
-
|
778
|
+
model.columns_hash[scope.to_s]
|
779
|
+
end
|
780
|
+
|
781
|
+
def column_limit_for(attribute)
|
782
|
+
column_for(attribute).try(:limit)
|
783
|
+
end
|
784
|
+
|
785
|
+
def model
|
786
|
+
@given_record.class
|
787
|
+
end
|
788
|
+
|
789
|
+
def failure_message_preface
|
790
|
+
prefix = ''
|
791
|
+
|
792
|
+
if @existing_record_created
|
793
|
+
prefix << "After taking the given #{model.name}"
|
794
|
+
|
795
|
+
if attribute_setter_for_existing_record
|
796
|
+
prefix << ', setting its '
|
797
|
+
prefix << description_for_attribute_setter(
|
798
|
+
attribute_setter_for_existing_record
|
799
|
+
)
|
800
|
+
else
|
801
|
+
prefix << ", whose :#{attribute} is "
|
802
|
+
prefix << "‹#{existing_value_read.inspect}›"
|
803
|
+
end
|
804
|
+
|
805
|
+
prefix << ", and saving it as the existing record, then"
|
806
|
+
else
|
807
|
+
if attribute_setter_for_existing_record
|
808
|
+
prefix << "Given an existing #{model.name},"
|
809
|
+
prefix << ' after setting its '
|
810
|
+
prefix << description_for_attribute_setter(
|
811
|
+
attribute_setter_for_existing_record
|
812
|
+
)
|
813
|
+
prefix << ', then'
|
814
|
+
else
|
815
|
+
prefix << "Given an existing #{model.name} whose :#{attribute}"
|
816
|
+
prefix << ' is '
|
817
|
+
prefix << Shoulda::Matchers::Util.inspect_value(
|
818
|
+
existing_value_read
|
819
|
+
)
|
820
|
+
prefix << ', after'
|
821
|
+
end
|
822
|
+
end
|
823
|
+
|
824
|
+
prefix << " making a new #{model.name} and setting its "
|
825
|
+
|
826
|
+
prefix << description_for_attribute_setter(
|
827
|
+
last_attribute_setter_used_on_new_record,
|
828
|
+
same_as_existing: existing_and_new_values_are_same?
|
829
|
+
)
|
830
|
+
|
831
|
+
prefix << ", the matcher expected the new #{model.name} to be"
|
832
|
+
|
833
|
+
prefix
|
834
|
+
end
|
835
|
+
|
836
|
+
def attribute_changed_value_message
|
837
|
+
<<-MESSAGE.strip
|
838
|
+
As indicated in the message above,
|
839
|
+
:#{last_attribute_setter_used_on_new_record.attribute_name} seems to be
|
840
|
+
changing certain values as they are set, and this could have something
|
841
|
+
to do with why this test is failing. If you or something else has
|
842
|
+
overridden the writer method for this attribute to normalize values by
|
843
|
+
changing their case in any way (for instance, ensuring that the
|
844
|
+
attribute is always downcased), then try adding
|
845
|
+
`ignoring_case_sensitivity` onto the end of the uniqueness matcher.
|
846
|
+
Otherwise, you may need to write the test yourself, or do something
|
847
|
+
different altogether.
|
848
|
+
MESSAGE
|
849
|
+
end
|
850
|
+
|
851
|
+
def description_for_attribute_setter(attribute_setter, same_as_existing: nil)
|
852
|
+
description = ":#{attribute_setter.attribute_name} to "
|
853
|
+
|
854
|
+
if same_as_existing == false
|
855
|
+
description << 'a different value, '
|
856
|
+
end
|
857
|
+
|
858
|
+
description << Shoulda::Matchers::Util.inspect_value(
|
859
|
+
attribute_setter.value_written
|
860
|
+
)
|
861
|
+
|
862
|
+
if attribute_setter.attribute_changed_value?
|
863
|
+
description << ' (read back as '
|
864
|
+
description << Shoulda::Matchers::Util.inspect_value(
|
865
|
+
attribute_setter.value_read
|
866
|
+
)
|
867
|
+
description << ')'
|
868
|
+
end
|
869
|
+
|
870
|
+
if same_as_existing == true
|
871
|
+
description << ' as well'
|
872
|
+
end
|
873
|
+
|
874
|
+
description
|
875
|
+
end
|
876
|
+
|
877
|
+
def existing_and_new_values_are_same?
|
878
|
+
last_value_set_on_new_record == existing_value_written
|
879
|
+
end
|
880
|
+
|
881
|
+
def last_attribute_setter_used_on_new_record
|
882
|
+
last_submatcher_run.last_attribute_setter_used
|
883
|
+
end
|
884
|
+
|
885
|
+
def last_value_set_on_new_record
|
886
|
+
last_submatcher_run.last_value_set
|
561
887
|
end
|
562
888
|
|
563
889
|
# @private
|