Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ignoring case sensitivity to uniqueness validation matcher #840

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,35 @@ module ActiveRecord
# should validate_uniqueness_of(:key).case_insensitive
# 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. Use this if the
# model modifies the case of the attribute before setting it on the model
# e.g. with a custom setter or as part of validation.
#
# class Post < ActiveRecord::Base
# validates_uniqueness_of :key
#
# def key=(value)
# super value.downcase
# end
# end
#
# # RSpec
# describe Post do
# it { should validate_uniqueness_of(:key).ignoring_case_sensitivity }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line is too long. [82/80]

# end
#
# # Minitest (Shoulda)
# class PostTest < ActiveSupport::TestCase
# should validate_uniqueness_of(:key).ignoring_case_sensitivity
# end
#
# ##### allow_nil
#
# Use `allow_nil` to assert that the attribute allows nil.
Expand Down Expand Up @@ -218,6 +247,7 @@ class ValidateUniquenessOfMatcher < ActiveModel::ValidationMatcher
def initialize(attribute)
super(attribute)
@options = {}
@options[:case] = :sensitive
end

def scoped_to(*scopes)
Expand All @@ -231,7 +261,12 @@ def with_message(message)
end

def case_insensitive
@options[:case_insensitive] = true
@options[:case] = :insensitive
self
end

def ignoring_case_sensitivity
@options[:case] = :ignore
self
end

Expand All @@ -247,7 +282,7 @@ def allow_blank

def description
result = "require "
result << "case sensitive " unless @options[:case_insensitive]
result << case_description
result << "unique value for #{@attribute}"
result << " scoped to #{@options[:scopes].join(', ')}" if @options[:scopes].present?
result
Expand All @@ -272,6 +307,17 @@ def matches?(subject)

private

def case_description
case @options[:case]
when :sensitive
'case sensitive '
when :insensitive
'case insensitive '
else
''
end
end

def validation
@subject.class._validators[@attribute].detect do |validator|
validator.is_a?(::ActiveRecord::Validations::UniquenessValidator)
Expand Down Expand Up @@ -409,9 +455,10 @@ def validate_case_sensitivity?
if value.respond_to?(:swapcase)
swapcased_value = value.swapcase

if @options[:case_insensitive]
case @options[:case]
when :insensitive
disallows_value_of(swapcased_value, @expected_message)
else
when :sensitive
if value == swapcased_value
raise NonCaseSwappableValueError.create(
model: @subject.class,
Expand All @@ -421,6 +468,8 @@ def validate_case_sensitivity?
end

allows_value_of(swapcased_value, @expected_message)
else
true
end
else
true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,51 @@
end
end

context 'when the writer method for the attribute changes incoming values' do
context 'and ignoring_case_sensitivity is not specified' do
it 'raises a CouldNotSetAttributeError' do
model = define_model_validating_uniqueness(
attribute_name: :name,
validation_options: { case_sensitive: false },
)

model.class_eval do
def name=(name)
super(name.upcase)
end
end

assertion = lambda do
expect(model.new).
to validate_uniqueness_of(:name).
case_insensitive
end

expect(&assertion).to raise_error(
Shoulda::Matchers::ActiveModel::AllowValueMatcher::CouldNotSetAttributeError

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line is too long. [86/80]
Put a comma after the last parameter of a multiline method call.

)
end
end

context 'and ignoring_case_sensitivity is specified' do
it 'accepts (and not raise an error)' do
model = define_model_validating_uniqueness(
attribute_name: :name,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm noticing that in the first test, you have the validation qualified with case_sensitive: false, but you don't have that here.

I'm kind of forgetting what the original bug was here. Is that something we need to have here, you think? Or, should we have two sets of tests, one where the validation is case_sensitive: true, one where it's case_sensitive: false?

)

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

let(:model_attributes) { {} }

def default_attribute
Expand Down Expand Up @@ -846,6 +891,7 @@ def determine_scope_attribute_names_from(scope_attributes)
end

def define_model_validating_uniqueness(options = {}, &block)
attribute_name = options.fetch(:attribute_name) { self.attribute_name }
attribute_type = options.fetch(:attribute_type, :string)
attribute_options = options.fetch(:attribute_options, {})
attribute = {
Expand Down