shoulda-matchers 3.0.1 → 3.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.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +3 -3
  4. data/CONTRIBUTING.md +60 -28
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +15 -12
  7. data/NEWS.md +111 -0
  8. data/README.md +94 -6
  9. data/Rakefile +10 -8
  10. data/custom_plan.rb +88 -0
  11. data/gemfiles/4.0.0.gemfile +1 -0
  12. data/gemfiles/4.0.0.gemfile.lock +21 -18
  13. data/gemfiles/4.0.1.gemfile +1 -0
  14. data/gemfiles/4.0.1.gemfile.lock +21 -18
  15. data/gemfiles/4.1.gemfile +1 -0
  16. data/gemfiles/4.1.gemfile.lock +21 -18
  17. data/gemfiles/4.2.gemfile +1 -0
  18. data/gemfiles/4.2.gemfile.lock +24 -21
  19. data/lib/shoulda/matchers/action_controller/permit_matcher.rb +6 -11
  20. data/lib/shoulda/matchers/active_model.rb +10 -1
  21. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +258 -180
  22. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_changed_value_error.rb +45 -0
  23. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_does_not_exist_error.rb +23 -0
  24. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setter.rb +236 -0
  25. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setter_and_validator.rb +62 -0
  26. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters.rb +40 -0
  27. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters_and_validators.rb +48 -0
  28. data/lib/shoulda/matchers/active_model/allow_value_matcher/successful_check.rb +14 -0
  29. data/lib/shoulda/matchers/active_model/allow_value_matcher/successful_setting.rb +14 -0
  30. data/lib/shoulda/matchers/active_model/disallow_value_matcher.rb +34 -14
  31. data/lib/shoulda/matchers/active_model/helpers.rb +9 -17
  32. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +13 -6
  33. data/lib/shoulda/matchers/active_model/numericality_matchers/even_number_matcher.rb +13 -2
  34. data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +19 -35
  35. data/lib/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher.rb +13 -2
  36. data/lib/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher.rb +12 -2
  37. data/lib/shoulda/matchers/active_model/qualifiers.rb +12 -0
  38. data/lib/shoulda/matchers/active_model/qualifiers/ignore_interference_by_writer.rb +101 -0
  39. data/lib/shoulda/matchers/active_model/qualifiers/ignoring_interference_by_writer.rb +21 -0
  40. data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +30 -32
  41. data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +5 -8
  42. data/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +22 -22
  43. data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +27 -16
  44. data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +58 -15
  45. data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +22 -12
  46. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +165 -87
  47. data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +7 -9
  48. data/lib/shoulda/matchers/active_model/validation_matcher.rb +111 -49
  49. data/lib/shoulda/matchers/active_model/validation_matcher/build_description.rb +60 -0
  50. data/lib/shoulda/matchers/active_model/validator.rb +71 -52
  51. data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +19 -5
  52. data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +450 -124
  53. data/lib/shoulda/matchers/util.rb +43 -0
  54. data/lib/shoulda/matchers/util/word_wrap.rb +59 -31
  55. data/lib/shoulda/matchers/version.rb +1 -1
  56. data/script/update_gem_in_all_appraisals +1 -1
  57. data/script/update_gems_in_all_appraisals +1 -1
  58. data/spec/acceptance/multiple_libraries_integration_spec.rb +5 -2
  59. data/spec/acceptance/rails_integration_spec.rb +6 -2
  60. data/spec/spec_helper.rb +1 -3
  61. data/spec/support/acceptance/helpers/step_helpers.rb +4 -1
  62. data/spec/support/tests/current_bundle.rb +21 -7
  63. data/spec/support/unit/active_record/create_table.rb +54 -0
  64. data/spec/support/unit/attribute.rb +47 -0
  65. data/spec/support/unit/capture.rb +6 -0
  66. data/spec/support/unit/change_value.rb +111 -0
  67. data/spec/support/unit/create_model_arguments/basic.rb +135 -0
  68. data/spec/support/unit/create_model_arguments/has_many.rb +15 -0
  69. data/spec/support/unit/create_model_arguments/uniqueness_matcher.rb +74 -0
  70. data/spec/support/unit/helpers/active_record_versions.rb +1 -1
  71. data/spec/support/unit/helpers/class_builder.rb +61 -47
  72. data/spec/support/unit/helpers/database_helpers.rb +5 -3
  73. data/spec/support/unit/helpers/model_builder.rb +77 -97
  74. data/spec/support/unit/helpers/validation_matcher_scenario_helpers.rb +44 -0
  75. data/spec/support/unit/load_environment.rb +12 -0
  76. data/spec/support/unit/matchers/fail_with_message_including_matcher.rb +2 -2
  77. data/spec/support/unit/matchers/fail_with_message_matcher.rb +12 -1
  78. data/spec/support/unit/model_creation_strategies/active_model.rb +111 -0
  79. data/spec/support/unit/model_creation_strategies/active_record.rb +77 -0
  80. data/spec/support/unit/model_creators.rb +19 -0
  81. data/spec/support/unit/model_creators/active_model.rb +39 -0
  82. data/spec/support/unit/model_creators/active_record.rb +43 -0
  83. data/spec/support/unit/model_creators/active_record/has_and_belongs_to_many.rb +95 -0
  84. data/spec/support/unit/model_creators/active_record/has_many.rb +67 -0
  85. data/spec/support/unit/model_creators/active_record/uniqueness_matcher.rb +42 -0
  86. data/spec/support/unit/model_creators/basic.rb +97 -0
  87. data/spec/support/unit/rails_application.rb +1 -1
  88. data/spec/support/unit/record_validating_confirmation_builder.rb +3 -7
  89. data/spec/support/unit/shared_examples/ignoring_interference_by_writer.rb +79 -0
  90. data/spec/support/unit/validation_matcher_scenario.rb +62 -0
  91. data/spec/unit/shoulda/matchers/active_model/allow_mass_assignment_of_matcher_spec.rb +4 -0
  92. data/spec/unit/shoulda/matchers/active_model/allow_value_matcher_spec.rb +575 -140
  93. data/spec/unit/shoulda/matchers/active_model/validate_absence_of_matcher_spec.rb +115 -15
  94. data/spec/unit/shoulda/matchers/active_model/validate_acceptance_of_matcher_spec.rb +42 -4
  95. data/spec/unit/shoulda/matchers/active_model/validate_confirmation_of_matcher_spec.rb +92 -6
  96. data/spec/unit/shoulda/matchers/active_model/validate_exclusion_of_matcher_spec.rb +122 -10
  97. data/spec/unit/shoulda/matchers/active_model/validate_inclusion_of_matcher_spec.rb +306 -58
  98. data/spec/unit/shoulda/matchers/active_model/validate_length_of_matcher_spec.rb +122 -3
  99. data/spec/unit/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +805 -131
  100. data/spec/unit/shoulda/matchers/active_model/validate_presence_of_matcher_spec.rb +196 -29
  101. data/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb +82 -40
  102. data/spec/unit/shoulda/matchers/active_record/validate_uniqueness_of_matcher_spec.rb +600 -101
  103. data/spec/unit/shoulda/matchers/util/word_wrap_spec.rb +88 -33
  104. data/spec/unit_spec_helper.rb +10 -22
  105. data/zeus.json +11 -0
  106. metadata +64 -23
  107. data/lib/shoulda/matchers/active_model/strict_validator.rb +0 -51
  108. data/spec/support/unit/shared_examples/numerical_type_submatcher.rb +0 -15
  109. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/comparison_matcher_spec.rb +0 -288
  110. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/even_number_matcher_spec.rb +0 -100
  111. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher_spec.rb +0 -100
  112. 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 require case sensitive unique value for title
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: :user_id
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
- @options = {}
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 with_message(message)
229
- @expected_message = message
272
+ def case_insensitive
273
+ @options[:case_sensitivity_strategy] = :insensitive
230
274
  self
231
275
  end
232
276
 
233
- def case_insensitive
234
- @options[:case_insensitive] = true
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 description
249
- result = "require "
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 matches?(subject)
257
- @original_subject = subject
258
- @subject = subject.class.new
259
- @expected_message ||= :taken
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
- scopes_match? &&
263
- set_scoped_attributes &&
264
- validate_everything_except_duplicate_nils_or_blanks? &&
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
- private
329
+ protected
274
330
 
275
- def validation
276
- @subject.class._validators[@attribute].detect do |validator|
277
- validator.is_a?(::ActiveRecord::Validations::UniquenessValidator)
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
- def scopes_match?
282
- expected_scopes = Array.wrap(@options[:scopes])
347
+ private
283
348
 
284
- if validation
285
- actual_scopes = Array.wrap(validation.options[:scope])
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
- actual_scopes = []
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
- @failure_message = "Expected validation to be scoped to " +
294
- "#{expected_scopes}"
383
+ @failure_reason = 'Expected the validation'
295
384
 
296
- if actual_scopes.present?
297
- @failure_message << ", but it was scoped to #{actual_scopes}."
385
+ if expected_scopes.empty?
386
+ @failure_reason << ' not to be scoped to anything'
298
387
  else
299
- @failure_message << ", but it was not scoped to anything."
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 @options[:allow_nil]
308
- ensure_nil_record_in_database
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 @options[:allow_blank]
317
- ensure_blank_record_in_database
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 existing_record
325
- @existing_record ||= first_instance
326
- end
327
-
328
- def first_instance
329
- @subject.class.first || create_record_in_database
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 ensure_nil_record_in_database
333
- unless existing_record_is_nil?
334
- create_record_in_database(nil_value: true)
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 ensure_blank_record_in_database
339
- unless existing_record_is_blank?
340
- create_record_in_database(blank_value: true)
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 existing_record_is_nil?
345
- @existing_record.present? && existing_value.nil?
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 existing_record_is_blank?
349
- @existing_record.present? && existing_value.strip == ''
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 create_record_in_database(options = {})
353
- @original_subject.tap do |instance|
354
- instance.__send__("#{@attribute}=", value_for_new_record(options))
355
- ensure_secure_password_set(instance)
356
- instance.save(validate: false)
357
- @created_record = instance
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 value_for_new_record(options = {})
369
- case
370
- when options[:nil_value] then nil
371
- when options[:blank_value] then ''
372
- else 'a'
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
- @subject.class.ancestors.map(&:to_s).include?('ActiveModel::SecurePassword::InstanceMethodsOnActivation')
517
+ model.ancestors.map(&:to_s).include?(
518
+ 'ActiveModel::SecurePassword::InstanceMethodsOnActivation'
519
+ )
378
520
  end
379
521
 
380
- def set_scoped_attributes
381
- if @options[:scopes].present?
382
- @options[:scopes].all? do |scope|
383
- setter = :"#{scope}="
384
- if @subject.respond_to?(setter)
385
- @subject.__send__(setter, existing_record.__send__(scope))
386
- true
387
- else
388
- @failure_message = "#{class_name} doesn't seem to have a #{scope} attribute."
389
- false
390
- end
391
- end
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 validate_everything_except_duplicate_nils_or_blanks?
398
- if (@options[:allow_nil] && existing_value.nil?) ||
399
- (@options[:allow_blank] && existing_value.blank?)
400
- create_record_with_value
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
- disallows_value_of(existing_value, @expected_message)
577
+ def inspected_scopes_missing_on_model
578
+ scopes_missing_on_model.map(&:inspect)
404
579
  end
405
580
 
406
- def validate_case_sensitivity?
407
- value = existing_value
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
- if value.respond_to?(:swapcase)
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 @options[:case_insensitive]
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: @subject.class,
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 create_record_with_value
431
- @existing_record = create_record_in_database
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 @options[:scopes].blank? || all_scopes_are_booleans?
625
+ if expected_scopes.empty? || all_scopes_are_booleans?
442
626
  true
443
627
  else
444
- @options[:scopes].all? do |scope|
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
- @subject.__send__("#{scope}=", next_value)
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
- @failure_message_when_negated <<
460
- " (with different value of #{scope})"
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
- case column.type
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
- @subject.class.respond_to?(:defined_enums) &&
538
- @subject.defined_enums[scope.to_s]
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
- @subject.defined_enums[scope.to_s].reject do |key, _|
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 existing_value
552
- existing_record.__send__(@attribute)
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 class_name
556
- @subject.class.name
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
- @subject.class.columns_hash[scope.to_s]
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