(feature): Add 'ignore_case_sensitivity' option
Fixes https://github.com/thoughtbot/shoulda-matchers/issues/836 also possibly https://github.com/thoughtbot/shoulda-matchers/issues/838 ? The issue is that the existing functionality was either actively asserting case sensitivity or case insensitivity. This commit adds an option to not assert either. This allows handling of the scenario where the case of an attribute is changed at some point before being assigned on the model.
This commit is contained in:
parent
68dd70a23d
commit
d7d96ad207
7
NEWS.md
7
NEWS.md
|
@ -24,6 +24,13 @@
|
||||||
* Add a test for `validate_numericality_of` so that it officially supports money
|
* Add a test for `validate_numericality_of` so that it officially supports money
|
||||||
columns.
|
columns.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add a new qualifier, `ignoring_case_sensitivity`, to `validate_uniqueness_of`.
|
||||||
|
This provides a way to test uniqueness of an attribute whose case is
|
||||||
|
normalized, either in a custom writer method for that attribute, or in a
|
||||||
|
custom `before_validation` callback.
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
|
||||||
* Improve failure messages and descriptions of all matchers across the board so
|
* Improve failure messages and descriptions of all matchers across the board so
|
||||||
|
|
|
@ -316,7 +316,11 @@ module Shoulda
|
||||||
:instance
|
:instance
|
||||||
)
|
)
|
||||||
|
|
||||||
attr_writer :failure_message_preface, :values_to_preset
|
attr_writer(
|
||||||
|
:attribute_changed_value_message,
|
||||||
|
:failure_message_preface,
|
||||||
|
:values_to_preset,
|
||||||
|
)
|
||||||
|
|
||||||
def initialize(*values)
|
def initialize(*values)
|
||||||
super
|
super
|
||||||
|
@ -418,7 +422,7 @@ module Shoulda
|
||||||
end
|
end
|
||||||
|
|
||||||
if include_attribute_changed_value_message?
|
if include_attribute_changed_value_message?
|
||||||
message << "\n\n" + attribute_changed_value_message
|
message << "\n\n" + attribute_changed_value_message.call
|
||||||
end
|
end
|
||||||
|
|
||||||
Shoulda::Matchers.word_wrap(message)
|
Shoulda::Matchers.word_wrap(message)
|
||||||
|
@ -487,22 +491,12 @@ module Shoulda
|
||||||
end
|
end
|
||||||
|
|
||||||
if include_attribute_changed_value_message?
|
if include_attribute_changed_value_message?
|
||||||
message << "\n\n" + attribute_changed_value_message
|
message << "\n\n" + attribute_changed_value_message.call
|
||||||
end
|
end
|
||||||
|
|
||||||
Shoulda::Matchers.word_wrap(message)
|
Shoulda::Matchers.word_wrap(message)
|
||||||
end
|
end
|
||||||
|
|
||||||
def attribute_changed_value_message
|
|
||||||
<<-MESSAGE.strip
|
|
||||||
As indicated in the message above, :#{result.attribute_setter.attribute_name}
|
|
||||||
seems to be changing certain values as they are set, and this could have
|
|
||||||
something to do with why this test is failing. If you've overridden the writer
|
|
||||||
method for this attribute, then you may need to change it to make this test
|
|
||||||
pass, or do something else entirely.
|
|
||||||
MESSAGE
|
|
||||||
end
|
|
||||||
|
|
||||||
def description
|
def description
|
||||||
ValidationMatcher::BuildDescription.call(self, simple_description)
|
ValidationMatcher::BuildDescription.call(self, simple_description)
|
||||||
end
|
end
|
||||||
|
@ -564,6 +558,26 @@ pass, or do something else entirely.
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def include_attribute_changed_value_message?
|
||||||
|
!ignore_interference_by_writer.never? &&
|
||||||
|
result.attribute_setter.attribute_changed_value?
|
||||||
|
end
|
||||||
|
|
||||||
|
def attribute_changed_value_message
|
||||||
|
@attribute_changed_value_message ||
|
||||||
|
method(:default_attribute_changed_value_message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_attribute_changed_value_message
|
||||||
|
<<-MESSAGE.strip
|
||||||
|
As indicated in the message above, :#{result.attribute_setter.attribute_name}
|
||||||
|
seems to be changing certain values as they are set, and this could have
|
||||||
|
something to do with why this test is failing. If you've overridden the writer
|
||||||
|
method for this attribute, then you may need to change it to make this test
|
||||||
|
pass, or do something else entirely.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
|
||||||
def descriptions_for_preset_values
|
def descriptions_for_preset_values
|
||||||
attribute_setters_for_values_to_preset.
|
attribute_setters_for_values_to_preset.
|
||||||
map(&:attribute_setter_description)
|
map(&:attribute_setter_description)
|
||||||
|
@ -586,11 +600,6 @@ pass, or do something else entirely.
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def include_attribute_changed_value_message?
|
|
||||||
!ignore_interference_by_writer.never? &&
|
|
||||||
result.attribute_setter.attribute_changed_value?
|
|
||||||
end
|
|
||||||
|
|
||||||
def inspected_values_to_set
|
def inspected_values_to_set
|
||||||
Shoulda::Matchers::Util.inspect_values(values_to_set).to_sentence(
|
Shoulda::Matchers::Util.inspect_values(values_to_set).to_sentence(
|
||||||
two_words_connector: " or ",
|
two_words_connector: " or ",
|
||||||
|
|
|
@ -10,6 +10,7 @@ module Shoulda
|
||||||
def_delegators(
|
def_delegators(
|
||||||
:allow_matcher,
|
:allow_matcher,
|
||||||
:_after_setting_value,
|
:_after_setting_value,
|
||||||
|
:attribute_changed_value_message=,
|
||||||
:attribute_to_set,
|
:attribute_to_set,
|
||||||
:description,
|
:description,
|
||||||
:expects_strict?,
|
:expects_strict?,
|
||||||
|
|
|
@ -167,6 +167,40 @@ module Shoulda
|
||||||
# should validate_uniqueness_of(:key).case_insensitive
|
# should validate_uniqueness_of(:key).case_insensitive
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
|
# ##### ignoring_case_sensitivity
|
||||||
|
#
|
||||||
|
# By default, `validate_uniqueness_of` will check that the
|
||||||
|
# validation is case sensitive: it asserts that uniquable attributes pass
|
||||||
|
# validation when their values are in a different case than corresponding
|
||||||
|
# attributes in the pre-existing record.
|
||||||
|
#
|
||||||
|
# Use `ignoring_case_sensitivity` to skip this check. This qualifier is
|
||||||
|
# particularly handy if your model has somehow changed the behavior of
|
||||||
|
# attribute you're testing so that it modifies the case of incoming values
|
||||||
|
# as they are set. For instance, perhaps you've overridden the writer
|
||||||
|
# method or added a `before_validation` callback to normalize the
|
||||||
|
# attribute.
|
||||||
|
#
|
||||||
|
# class User < ActiveRecord::Base
|
||||||
|
# validates_uniqueness_of :email
|
||||||
|
#
|
||||||
|
# def email=(value)
|
||||||
|
# super(value.downcase)
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# # RSpec
|
||||||
|
# describe Post do
|
||||||
|
# it do
|
||||||
|
# should validate_uniqueness_of(:email).ignoring_case_sensitivity
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# # Minitest (Shoulda)
|
||||||
|
# class PostTest < ActiveSupport::TestCase
|
||||||
|
# should validate_uniqueness_of(:email).ignoring_case_sensitivity
|
||||||
|
# end
|
||||||
|
#
|
||||||
# ##### allow_nil
|
# ##### allow_nil
|
||||||
#
|
#
|
||||||
# Use `allow_nil` to assert that the attribute allows nil.
|
# Use `allow_nil` to assert that the attribute allows nil.
|
||||||
|
@ -218,7 +252,9 @@ module Shoulda
|
||||||
def initialize(attribute)
|
def initialize(attribute)
|
||||||
super(attribute)
|
super(attribute)
|
||||||
@expected_message = :taken
|
@expected_message = :taken
|
||||||
@options = {}
|
@options = {
|
||||||
|
case_sensitivity_strategy: :sensitive
|
||||||
|
}
|
||||||
@existing_record_created = false
|
@existing_record_created = false
|
||||||
@failure_reason = nil
|
@failure_reason = nil
|
||||||
@failure_reason_when_negated = nil
|
@failure_reason_when_negated = nil
|
||||||
|
@ -234,7 +270,12 @@ module Shoulda
|
||||||
end
|
end
|
||||||
|
|
||||||
def case_insensitive
|
def case_insensitive
|
||||||
@options[:case_insensitive] = true
|
@options[:case_sensitivity_strategy] = :insensitive
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def ignoring_case_sensitivity
|
||||||
|
@options[:case_sensitivity_strategy] = :ignore
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -258,13 +299,7 @@ module Shoulda
|
||||||
|
|
||||||
def simple_description
|
def simple_description
|
||||||
description = "validate that :#{@attribute} is"
|
description = "validate that :#{@attribute} is"
|
||||||
|
description << description_for_case_sensitive_qualifier
|
||||||
if @options[:case_insensitive]
|
|
||||||
description << ' case-insensitively'
|
|
||||||
else
|
|
||||||
description << ' case-sensitively'
|
|
||||||
end
|
|
||||||
|
|
||||||
description << ' unique'
|
description << ' unique'
|
||||||
|
|
||||||
if @options[:scopes].present?
|
if @options[:scopes].present?
|
||||||
|
@ -304,11 +339,17 @@ module Shoulda
|
||||||
def build_allow_or_disallow_value_matcher(args)
|
def build_allow_or_disallow_value_matcher(args)
|
||||||
super.tap do |matcher|
|
super.tap do |matcher|
|
||||||
matcher.failure_message_preface = method(:failure_message_preface)
|
matcher.failure_message_preface = method(:failure_message_preface)
|
||||||
|
matcher.attribute_changed_value_message =
|
||||||
|
method(:attribute_changed_value_message)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def case_sensitivity_strategy
|
||||||
|
@options[:case_sensitivity_strategy]
|
||||||
|
end
|
||||||
|
|
||||||
def new_record
|
def new_record
|
||||||
unless defined?(@new_record)
|
unless defined?(@new_record)
|
||||||
build_new_record
|
build_new_record
|
||||||
|
@ -318,6 +359,17 @@ module Shoulda
|
||||||
end
|
end
|
||||||
alias_method :subject, :new_record
|
alias_method :subject, :new_record
|
||||||
|
|
||||||
|
def description_for_case_sensitive_qualifier
|
||||||
|
case case_sensitivity_strategy
|
||||||
|
when :sensitive
|
||||||
|
' case-sensitively'
|
||||||
|
when :insensitive
|
||||||
|
' case-insensitively'
|
||||||
|
else
|
||||||
|
''
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def validation
|
def validation
|
||||||
model._validators[@attribute].detect do |validator|
|
model._validators[@attribute].detect do |validator|
|
||||||
validator.is_a?(::ActiveRecord::Validations::UniquenessValidator)
|
validator.is_a?(::ActiveRecord::Validations::UniquenessValidator)
|
||||||
|
@ -535,14 +587,11 @@ module Shoulda
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_case_sensitivity?
|
def validate_case_sensitivity?
|
||||||
|
if should_validate_case_sensitivity?
|
||||||
value = existing_value_read
|
value = existing_value_read
|
||||||
|
|
||||||
if value.respond_to?(:swapcase) && !value.empty?
|
|
||||||
swapcased_value = value.swapcase
|
swapcased_value = value.swapcase
|
||||||
|
|
||||||
if @options[:case_insensitive]
|
if case_sensitivity_strategy == :sensitive
|
||||||
disallows_value_of(swapcased_value, @expected_message)
|
|
||||||
else
|
|
||||||
if value == swapcased_value
|
if value == swapcased_value
|
||||||
raise NonCaseSwappableValueError.create(
|
raise NonCaseSwappableValueError.create(
|
||||||
model: model,
|
model: model,
|
||||||
|
@ -552,12 +601,20 @@ module Shoulda
|
||||||
end
|
end
|
||||||
|
|
||||||
allows_value_of(swapcased_value, @expected_message)
|
allows_value_of(swapcased_value, @expected_message)
|
||||||
|
else
|
||||||
|
disallows_value_of(swapcased_value, @expected_message)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def should_validate_case_sensitivity?
|
||||||
|
case_sensitivity_strategy != :ignore &&
|
||||||
|
existing_value_read.respond_to?(:swapcase) &&
|
||||||
|
!existing_value_read.empty?
|
||||||
|
end
|
||||||
|
|
||||||
def model_class?(model_name)
|
def model_class?(model_name)
|
||||||
model_name.constantize.ancestors.include?(::ActiveRecord::Base)
|
model_name.constantize.ancestors.include?(::ActiveRecord::Base)
|
||||||
rescue NameError
|
rescue NameError
|
||||||
|
@ -776,6 +833,21 @@ module Shoulda
|
||||||
prefix
|
prefix
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def attribute_changed_value_message
|
||||||
|
<<-MESSAGE.strip
|
||||||
|
As indicated in the message above,
|
||||||
|
:#{last_attribute_setter_used_on_new_record.attribute_name} seems to be
|
||||||
|
changing certain values as they are set, and this could have something
|
||||||
|
to do with why this test is failing. If you or something else has
|
||||||
|
overridden the writer method for this attribute to normalize values by
|
||||||
|
changing their case in any way (for instance, ensuring that the
|
||||||
|
attribute is always downcased), then try adding
|
||||||
|
`ignoring_case_sensitivity` onto the end of the uniqueness matcher.
|
||||||
|
Otherwise, you may need to write the test yourself, or do something
|
||||||
|
different altogether.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
|
||||||
def description_for_attribute_setter(attribute_setter, same_as_existing: nil)
|
def description_for_attribute_setter(attribute_setter, same_as_existing: nil)
|
||||||
description = ":#{attribute_setter.attribute_name} to "
|
description = ":#{attribute_setter.attribute_name} to "
|
||||||
|
|
||||||
|
|
|
@ -467,9 +467,13 @@ Example did not properly validate that :attr is case-sensitively unique.
|
||||||
|
|
||||||
As indicated in the message above, :attr seems to be changing certain
|
As indicated in the message above, :attr seems to be changing certain
|
||||||
values as they are set, and this could have something to do with why
|
values as they are set, and this could have something to do with why
|
||||||
this test is failing. If you've overridden the writer method for this
|
this test is failing. If you or something else has overridden the
|
||||||
attribute, then you may need to change it to make this test pass, or
|
writer method for this attribute to normalize values by changing their
|
||||||
do something else entirely.
|
case in any way (for instance, ensuring that the attribute is always
|
||||||
|
downcased), then try adding `ignoring_case_sensitivity` onto the end
|
||||||
|
of the uniqueness matcher. Otherwise, you may need to write the test
|
||||||
|
yourself, or do something different altogether.
|
||||||
|
|
||||||
MESSAGE
|
MESSAGE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -769,9 +773,12 @@ unique.
|
||||||
|
|
||||||
As indicated in the message above, :attr seems to be changing certain
|
As indicated in the message above, :attr seems to be changing certain
|
||||||
values as they are set, and this could have something to do with why
|
values as they are set, and this could have something to do with why
|
||||||
this test is failing. If you've overridden the writer method for this
|
this test is failing. If you or something else has overridden the
|
||||||
attribute, then you may need to change it to make this test pass, or
|
writer method for this attribute to normalize values by changing their
|
||||||
do something else entirely.
|
case in any way (for instance, ensuring that the attribute is always
|
||||||
|
downcased), then try adding `ignoring_case_sensitivity` onto the end
|
||||||
|
of the uniqueness matcher. Otherwise, you may need to write the test
|
||||||
|
yourself, or do something different altogether.
|
||||||
MESSAGE
|
MESSAGE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1124,6 +1131,90 @@ Example did not properly validate that :attr is case-sensitively unique.
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the writer method for the attribute changes the case of incoming values' do
|
||||||
|
context 'when the validation is case-sensitive' do
|
||||||
|
context 'and the matcher is ensuring that the validation is case-sensitive' do
|
||||||
|
it 'rejects with an appropriate failure message' do
|
||||||
|
model = define_model_validating_uniqueness(
|
||||||
|
attribute_name: :name
|
||||||
|
)
|
||||||
|
|
||||||
|
model.class_eval do
|
||||||
|
def name=(name)
|
||||||
|
super(name.upcase)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assertion = lambda do
|
||||||
|
expect(model.new).to validate_uniqueness_of(:name)
|
||||||
|
end
|
||||||
|
|
||||||
|
message = <<-MESSAGE.strip
|
||||||
|
Example did not properly validate that :name is case-sensitively unique.
|
||||||
|
After taking the given Example, setting its :name to ‹"an arbitrary
|
||||||
|
value"› (read back as ‹"AN ARBITRARY VALUE"›), and saving it as the
|
||||||
|
existing record, then making a new Example and setting its :name to
|
||||||
|
‹"an arbitrary value"› (read back as ‹"AN ARBITRARY VALUE"›) as well,
|
||||||
|
the matcher expected the new Example to be valid, but it was invalid
|
||||||
|
instead, producing these validation errors:
|
||||||
|
|
||||||
|
* name: ["has already been taken"]
|
||||||
|
|
||||||
|
As indicated in the message above, :name seems to be changing certain
|
||||||
|
values as they are set, and this could have something to do with why
|
||||||
|
this test is failing. If you or something else has overridden the
|
||||||
|
writer method for this attribute to normalize values by changing their
|
||||||
|
case in any way (for instance, ensuring that the attribute is always
|
||||||
|
downcased), then try adding `ignoring_case_sensitivity` onto the end
|
||||||
|
of the uniqueness matcher. Otherwise, you may need to write the test
|
||||||
|
yourself, or do something different altogether.
|
||||||
|
MESSAGE
|
||||||
|
|
||||||
|
expect(&assertion).to fail_with_message(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and the matcher is ignoring case sensitivity' do
|
||||||
|
it 'accepts (and not raise an error)' do
|
||||||
|
model = define_model_validating_uniqueness(
|
||||||
|
attribute_name: :name
|
||||||
|
)
|
||||||
|
|
||||||
|
model.class_eval do
|
||||||
|
def name=(name)
|
||||||
|
super(name.upcase)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(model.new).
|
||||||
|
to validate_uniqueness_of(:name).
|
||||||
|
ignoring_case_sensitivity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the validation is case-insensitive' do
|
||||||
|
context 'and the matcher is ensuring that the validation is case-insensitive' do
|
||||||
|
it 'accepts (and does not raise an error)' do
|
||||||
|
model = define_model_validating_uniqueness(
|
||||||
|
attribute_name: :name,
|
||||||
|
validation_options: { case_sensitive: false },
|
||||||
|
)
|
||||||
|
|
||||||
|
model.class_eval do
|
||||||
|
def name=(name)
|
||||||
|
super(name.downcase)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(model.new).
|
||||||
|
to validate_uniqueness_of(:name).
|
||||||
|
case_insensitive
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
let(:model_attributes) { {} }
|
let(:model_attributes) { {} }
|
||||||
|
|
||||||
def default_attribute
|
def default_attribute
|
||||||
|
|
Loading…
Reference in New Issue