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.
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