thoughtbot--shoulda-matchers/lib/shoulda/matchers/active_record/validate_uniqueness_of_matc...

1149 lines
36 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

module Shoulda
module Matchers
module ActiveRecord
# The `validate_uniqueness_of` matcher tests usage of the
# `validates_uniqueness_of` validation. It first checks for an existing
# instance of your model in the database, creating one if necessary. It
# then takes a new instance of that model and asserts that it fails
# validation if the attribute or attributes you've specified in the
# validation are set to values which are the same as those of the
# pre-existing record (thereby failing the uniqueness check).
#
# class Post < ActiveRecord::Base
# validates :permalink, uniqueness: true
# end
#
# # RSpec
# RSpec.describe Post, type: :model do
# it { should validate_uniqueness_of(:permalink) }
# end
#
# # Minitest (Shoulda)
# class PostTest < ActiveSupport::TestCase
# should validate_uniqueness_of(:permalink)
# end
#
# #### Caveat
#
# This matcher works a bit differently than other matchers. As noted
# before, it will create an instance of your model if one doesn't already
# exist. Sometimes this step fails, especially if you have database-level
# restrictions on any attributes other than the one which is unique. In
# this case, the solution is to populate these attributes with values
# before you call `validate_uniqueness_of`.
#
# For example, say you have the following migration and model:
#
# class CreatePosts < ActiveRecord::Migration
# def change
# create_table :posts do |t|
# t.string :title
# t.text :content, null: false
# end
# end
# end
#
# class Post < ActiveRecord::Base
# validates :title, uniqueness: true
# end
#
# You may be tempted to test the model like this:
#
# RSpec.describe Post, type: :model do
# it { should validate_uniqueness_of(:title) }
# end
#
# However, running this test will fail with an exception such as:
#
# Shoulda::Matchers::ActiveRecord::ValidateUniquenessOfMatcher::ExistingRecordInvalid:
# validate_uniqueness_of works by matching a new record against an
# existing record. If there is no existing record, it will create one
# using the record you provide.
#
# While doing this, the following error was raised:
#
# PG::NotNullViolation: ERROR: null value in column "content" violates not-null constraint
# DETAIL: Failing row contains (1, null, null).
# : INSERT INTO "posts" DEFAULT VALUES RETURNING "id"
#
# The best way to fix this is to provide the matcher with a record where
# any required attributes are filled in with valid values beforehand.
#
# (The exact error message will differ depending on which database you're
# using, but you get the idea.)
#
# This happens because `validate_uniqueness_of` tries to create a new post
# but cannot do so because of the `content` attribute: though unrelated to
# this test, it nevertheless needs to be filled in. As indicated at the
# end of the error message, the solution is to build a custom Post object
# ahead of time with `content` filled in:
#
# RSpec.describe Post, type: :model do
# describe "validations" do
# subject { Post.new(content: "Here is the content") }
# it { should validate_uniqueness_of(:title) }
# end
# end
#
# Or, if you're using
# [FactoryBot](https://github.com/thoughtbot/factory_bot) and you have a
# `post` factory defined which automatically fills in `content`, you can
# say:
#
# RSpec.describe Post, type: :model do
# describe "validations" do
# subject { FactoryBot.build(:post) }
# it { should validate_uniqueness_of(:title) }
# end
# end
#
# #### Qualifiers
#
# Use `on` if your validation applies only under a certain context.
#
# class Post < ActiveRecord::Base
# validates :title, uniqueness: true, on: :create
# end
#
# # RSpec
# RSpec.describe Post, type: :model do
# it { should validate_uniqueness_of(:title).on(:create) }
# end
#
# # Minitest (Shoulda)
# class PostTest < ActiveSupport::TestCase
# should validate_uniqueness_of(:title).on(:create)
# end
#
# ##### with_message
#
# Use `with_message` if you are using a custom validation message.
#
# class Post < ActiveRecord::Base
# validates :title, uniqueness: true, message: 'Please choose another title'
# end
#
# # RSpec
# RSpec.describe Post, type: :model do
# it do
# should validate_uniqueness_of(:title).
# with_message('Please choose another title')
# end
# end
#
# # Minitest (Shoulda)
# class PostTest < ActiveSupport::TestCase
# should validate_uniqueness_of(:title).
# with_message('Please choose another title')
# end
#
# ##### scoped_to
#
# Use `scoped_to` to test usage of the `:scope` option. This asserts that
# a new record fails validation if not only the primary attribute is not
# unique, but the scoped attributes are not unique either.
#
# class Post < ActiveRecord::Base
# validates :slug, uniqueness: { scope: :journal_id }
# end
#
# # RSpec
# RSpec.describe Post, type: :model do
# it { should validate_uniqueness_of(:slug).scoped_to(:journal_id) }
# end
#
# # Minitest (Shoulda)
# class PostTest < ActiveSupport::TestCase
# should validate_uniqueness_of(:slug).scoped_to(:journal_id)
# end
#
# NOTE: Support for testing uniqueness validation scoped to an array of
# associations is not available.
#
# For more information, please refer to
# https://github.com/thoughtbot/shoulda-matchers/issues/814
#
# ##### case_insensitive
#
# Use `case_insensitive` to test usage of the `:case_sensitive` option
# with a false value. This asserts that the uniquable attributes fail
# validation even if their values are a different case than corresponding
# attributes in the pre-existing record.
#
# class Post < ActiveRecord::Base
# validates :key, uniqueness: { case_sensitive: false }
# end
#
# # RSpec
# RSpec.describe Post, type: :model do
# it { should validate_uniqueness_of(:key).case_insensitive }
# end
#
# # Minitest (Shoulda)
# class PostTest < ActiveSupport::TestCase
# 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. 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 :email, uniqueness: true
#
# def email=(value)
# super(value.downcase)
# end
# end
#
# # RSpec
# RSpec.describe Post, type: :model 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
#
# Use `allow_nil` to assert that the attribute allows nil.
#
# class Post < ActiveRecord::Base
# validates :author_id, uniqueness: true, allow_nil: true
# end
#
# # RSpec
# RSpec.describe Post, type: :model do
# it { should validate_uniqueness_of(:author_id).allow_nil }
# end
#
# # Minitest (Shoulda)
# class PostTest < ActiveSupport::TestCase
# should validate_uniqueness_of(:author_id).allow_nil
# end
#
# @return [ValidateUniquenessOfMatcher]
#
# ##### allow_blank
#
# Use `allow_blank` to assert that the attribute allows a blank value.
#
# class Post < ActiveRecord::Base
# validates :author_id, uniqueness: true, allow_blank: true
# end
#
# # RSpec
# RSpec.describe Post, type: :model do
# it { should validate_uniqueness_of(:author_id).allow_blank }
# end
#
# # Minitest (Shoulda)
# class PostTest < ActiveSupport::TestCase
# should validate_uniqueness_of(:author_id).allow_blank
# end
#
# @return [ValidateUniquenessOfMatcher]
#
def validate_uniqueness_of(attr)
ValidateUniquenessOfMatcher.new(attr)
end
# @private
class ValidateUniquenessOfMatcher < ActiveModel::ValidationMatcher
include ActiveModel::Helpers
def initialize(attribute)
super(attribute)
@expected_message = :taken
@options = {
case_sensitivity_strategy: :sensitive,
}
@existing_record_created = false
@failure_reason = nil
@failure_reason_when_negated = nil
@attribute_setters = {
existing_record: AttributeSetters.new,
new_record: AttributeSetters.new,
}
end
def scoped_to(*scopes)
@options[:scopes] = [*scopes].flatten.map(&:to_sym)
self
end
def case_insensitive
@options[:case_sensitivity_strategy] = :insensitive
self
end
def ignoring_case_sensitivity
@options[:case_sensitivity_strategy] = :ignore
self
end
def allow_nil
@options[:allow_nil] = true
self
end
def expects_to_allow_nil?
@options[:allow_nil] == true
end
def allow_blank
@options[:allow_blank] = true
self
end
def expects_to_allow_blank?
@options[:allow_blank] == true
end
def simple_description
description = "validate that :#{@attribute} is"
description << description_for_case_sensitive_qualifier
description << ' unique'
if @options[:scopes].present?
description << " within the scope of #{inspected_expected_scopes}"
end
description
end
def matches?(given_record)
@given_record = given_record
@all_records = model.all
matches_presence_of_attribute? &&
matches_presence_of_scopes? &&
matches_scopes_configuration? &&
matches_uniqueness_without_scopes? &&
matches_uniqueness_with_case_sensitivity_strategy? &&
matches_uniqueness_with_scopes? &&
matches_allow_nil? &&
matches_allow_blank?
ensure
Uniqueness::TestModels.remove_all
end
def does_not_match?(given_record)
@given_record = given_record
@all_records = model.all
does_not_match_presence_of_scopes? ||
does_not_match_scopes_configuration? ||
does_not_match_uniqueness_without_scopes? ||
does_not_match_uniqueness_with_case_sensitivity_strategy? ||
does_not_match_uniqueness_with_scopes? ||
does_not_match_allow_nil? ||
does_not_match_allow_blank?
ensure
Uniqueness::TestModels.remove_all
end
protected
def failure_reason
@failure_reason || super
end
def failure_reason_when_negated
@failure_reason_when_negated || super
end
def build_allow_or_disallow_value_matcher(args)
super.tap do |matcher|
matcher.failure_message_preface = method(:failure_message_preface)
matcher.attribute_changed_value_message =
method(:attribute_changed_value_message)
end
end
private
def case_sensitivity_strategy
@options[:case_sensitivity_strategy]
end
def new_record
unless defined?(@new_record)
build_new_record
end
@new_record
end
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 validations
model.validators_on(@attribute).select do |validator|
validator.is_a?(::ActiveRecord::Validations::UniquenessValidator)
end
end
def matches_scopes_configuration?
if scopes_match?
true
else
@failure_reason = 'Expected the validation '
@failure_reason <<
if expected_scopes.empty?
'not to be scoped to anything, '
else
"to be scoped to #{inspected_expected_scopes}, "
end
if actual_sets_of_scopes.any?
@failure_reason << 'but it was scoped to '
@failure_reason << "#{inspected_actual_scopes} instead."
else
@failure_reason << 'but it was not scoped to anything.'
end
false
end
end
def does_not_match_scopes_configuration?
if scopes_match?
@failure_reason = 'Expected the validation '
if expected_scopes.empty?
@failure_reason << 'to be scoped to nothing, '
@failure_reason << 'but it was scoped to '
@failure_reason << "#{inspected_actual_scopes} instead."
else
@failure_reason << 'not to be scoped to '
@failure_reason << inspected_expected_scopes
end
false
else
true
end
end
def scopes_match?
actual_sets_of_scopes.empty? && expected_scopes.empty? ||
actual_sets_of_scopes.any? { |scopes| scopes == expected_scopes }
end
def inspected_expected_scopes
expected_scopes.map(&:inspect).to_sentence
end
def inspected_actual_scopes
inspected_actual_sets_of_scopes.to_sentence(
words_connector: ' and ',
last_word_connector: ', and',
)
end
def inspected_actual_sets_of_scopes
inspected_sets_of_scopes = actual_sets_of_scopes.map do |scopes|
scopes.map(&:inspect)
end
if inspected_sets_of_scopes.many?
inspected_sets_of_scopes.map { |x| "(#{x.to_sentence})" }
else
inspected_sets_of_scopes.map(&:to_sentence)
end
end
def expected_scopes
Array.wrap(@options[:scopes])
end
def actual_sets_of_scopes
validations.map do |validation|
Array.wrap(validation.options[:scope]).map(&:to_sym)
end.reject(&:empty?)
end
def matches_allow_nil?
!expects_to_allow_nil? || (
update_existing_record!(nil) &&
allows_value_of(nil, @expected_message)
)
end
def does_not_match_allow_nil?
expects_to_allow_nil? && (
update_existing_record!(nil) &&
(@failure_reason = nil ||
disallows_value_of(nil, @expected_message)
)
)
end
def matches_allow_blank?
!expects_to_allow_blank? || (
update_existing_record!('') &&
allows_value_of('', @expected_message)
)
end
def does_not_match_allow_blank?
expects_to_allow_blank? && (
update_existing_record!('') &&
(@failure_reason = nil || disallows_value_of('', @expected_message))
)
end
def existing_record
unless defined?(@existing_record)
find_or_create_existing_record
end
@existing_record
end
def find_or_create_existing_record
@existing_record = find_existing_record
unless @existing_record
@existing_record = create_existing_record
@existing_record_created = true
end
end
def find_existing_record
model.first.presence
end
def create_existing_record
@given_record.tap do |existing_record|
existing_record.save(validate: false)
end
rescue ::ActiveRecord::StatementInvalid => e
raise ExistingRecordInvalid.create(underlying_exception: e)
end
def update_existing_record!(value)
if existing_value_read != value
set_attribute_on_existing_record!(@attribute, value)
# It would be nice if we could ensure that the record was valid,
# but that would break users' existing tests
existing_record.save(validate: false)
end
true
end
def arbitrary_non_blank_value
non_blank_value = dummy_value_for(@attribute)
limit = column_limit_for(@attribute)
is_string_value = non_blank_value.is_a?(String)
if is_string_value && limit && limit < non_blank_value.length
'x' * limit
else
non_blank_value
end
end
def has_secure_password?
Shoulda::Matchers::RailsShim.has_secure_password?(subject, @attribute)
end
def build_new_record
@new_record = existing_record.dup
attribute_names_under_test.each do |attribute_name|
set_attribute_on_new_record!(
attribute_name,
existing_record.public_send(attribute_name),
)
end
@new_record
end
def matches_presence_of_attribute?
if attribute_present_on_model?
true
else
@failure_reason =
":#{attribute} does not seem to be an attribute on #{model.name}."
false
end
end
def does_not_match_presence_of_attribute?
if attribute_present_on_model?
@failure_reason =
":#{attribute} seems to be an attribute on #{model.name}."
false
else
true
end
end
def attribute_present_on_model?
model.method_defined?("#{attribute}=") ||
model.columns_hash.key?(attribute.to_s)
end
def matches_presence_of_scopes?
if scopes_missing_on_model.none?
true
else
inspected_scopes = scopes_missing_on_model.map(&:inspect)
reason = ''
reason << inspected_scopes.to_sentence
reason <<
if inspected_scopes.many?
' do not seem to be attributes'
else
' does not seem to be an attribute'
end
reason << " on #{model.name}."
@failure_reason = reason
false
end
end
def does_not_match_presence_of_scopes?
if scopes_missing_on_model.any?
true
else
inspected_scopes = scopes_present_on_model.map(&:inspect)
reason = ''
reason << inspected_scopes.to_sentence
reason <<
if inspected_scopes.many?
' seem to be attributes'
else
' seems to be an attribute'
end
reason << " on #{model.name}."
@failure_reason = reason
false
end
end
def scopes_present_on_model
@_scopes_present_on_model ||= expected_scopes.select do |scope|
model.method_defined?("#{scope}=")
end
end
def scopes_missing_on_model
@_scopes_missing_on_model ||= expected_scopes.reject do |scope|
model.method_defined?("#{scope}=")
end
end
def matches_uniqueness_without_scopes?
if existing_value_read.blank?
update_existing_record!(arbitrary_non_blank_value)
end
disallows_value_of(existing_value_read, @expected_message)
end
def does_not_match_uniqueness_without_scopes?
@failure_reason = nil
if existing_value_read.blank?
update_existing_record!(arbitrary_non_blank_value)
end
allows_value_of(existing_value_read, @expected_message)
end
def matches_uniqueness_with_case_sensitivity_strategy?
if should_test_case_sensitivity?
value = existing_value_read
swapcased_value = value.swapcase
if case_sensitivity_strategy == :sensitive
if value == swapcased_value
raise NonCaseSwappableValueError.create(
model: model,
attribute: @attribute,
value: value,
)
end
allows_value_of(swapcased_value, @expected_message)
else
disallows_value_of(swapcased_value, @expected_message)
end
else
true
end
end
def does_not_match_uniqueness_with_case_sensitivity_strategy?
if should_test_case_sensitivity?
@failure_reason = nil
value = existing_value_read
swapcased_value = value.swapcase
if case_sensitivity_strategy == :sensitive
disallows_value_of(swapcased_value, @expected_message)
else
if value == swapcased_value
raise NonCaseSwappableValueError.create(
model: model,
attribute: @attribute,
value: value,
)
end
allows_value_of(swapcased_value, @expected_message)
end
else
true
end
end
def should_test_case_sensitivity?
case_sensitivity_strategy != :ignore &&
existing_value_read.respond_to?(:swapcase) &&
!existing_value_read.empty?
end
def model_class?(model_name)
model_name.constantize.ancestors.include?(::ActiveRecord::Base)
rescue NameError
false
end
def matches_uniqueness_with_scopes?
expected_scopes.none? ||
all_scopes_are_booleans? ||
expected_scopes.all? do |scope|
setting_next_value_for(scope) do
allows_value_of(existing_value_read, @expected_message)
end
end
end
def does_not_match_uniqueness_with_scopes?
expected_scopes.any? &&
!all_scopes_are_booleans? &&
expected_scopes.any? do |scope|
setting_next_value_for(scope) do
@failure_reason = nil
disallows_value_of(existing_value_read, @expected_message)
end
end
end
def setting_next_value_for(scope)
previous_value = @all_records.map(&scope).compact.max
next_value =
if previous_value.blank?
dummy_value_for(scope)
else
next_value_for(scope, previous_value)
end
set_attribute_on_new_record!(scope, next_value)
yield
ensure
set_attribute_on_new_record!(scope, previous_value)
end
def dummy_value_for(scope)
column = column_for(scope)
if column.respond_to?(:array) && column.array
[dummy_scalar_value_for(column)]
else
dummy_scalar_value_for(column)
end
end
def dummy_scalar_value_for(column)
Shoulda::Matchers::Util.dummy_value_for(column.type)
end
def next_value_for(scope, previous_value)
if previous_value.is_a?(Array)
[next_scalar_value_for(scope, previous_value[0])]
else
next_scalar_value_for(scope, previous_value)
end
end
def next_scalar_value_for(scope, previous_value)
column = column_for(scope)
if column.type == :uuid
SecureRandom.uuid
elsif defined_as_enum?(scope)
available_values = available_enum_values_for(scope, previous_value)
available_values.keys.last
elsif polymorphic_type_attribute?(scope, previous_value)
Uniqueness::TestModels.create(previous_value).to_s
elsif previous_value.respond_to?(:next)
previous_value.next
elsif previous_value.respond_to?(:to_datetime)
previous_value.to_datetime.in(60).next
elsif boolean_value?(previous_value)
!previous_value
else
previous_value.to_s.next
end
end
def all_scopes_are_booleans?
@options[:scopes].all? do |scope|
@all_records.map(&scope).all? { |s| boolean_value?(s) }
end
end
def boolean_value?(value)
[true, false].include?(value)
end
def defined_as_enum?(scope)
model.respond_to?(:defined_enums) &&
new_record.defined_enums[scope.to_s]
end
def polymorphic_type_attribute?(scope, previous_value)
scope.to_s =~ /_type$/ && model_class?(previous_value)
end
def available_enum_values_for(scope, previous_value)
new_record.defined_enums[scope.to_s].reject do |key, _|
key == previous_value
end
end
def set_attribute_on!(record_type, record, attribute_name, value)
attribute_setter = build_attribute_setter(
record,
attribute_name,
value,
)
attribute_setter.set!
@attribute_setters[record_type] << attribute_setter
end
def set_attribute_on_existing_record!(attribute_name, value)
set_attribute_on!(
:existing_record,
existing_record,
attribute_name,
value,
)
end
def set_attribute_on_new_record!(attribute_name, value)
set_attribute_on!(
:new_record,
new_record,
attribute_name,
value,
)
end
def attribute_setter_for_existing_record
@attribute_setters[:existing_record].last
end
def attribute_setters_for_new_record
@attribute_setters[:new_record] +
[last_attribute_setter_used_on_new_record]
end
def attribute_names_under_test
[@attribute] + expected_scopes
end
def build_attribute_setter(record, attribute_name, value)
Shoulda::Matchers::ActiveModel::AllowValueMatcher::AttributeSetter.
new(
matcher_name: :validate_uniqueness_of,
object: record,
attribute_name: attribute_name,
value: value,
ignore_interference_by_writer: ignore_interference_by_writer,
)
end
def existing_value_read
existing_record.public_send(@attribute)
end
def existing_value_written
if attribute_setter_for_existing_record
attribute_setter_for_existing_record.value_written
else
existing_value_read
end
end
def column_for(scope)
model.columns_hash[scope.to_s]
end
def column_limit_for(attribute)
column_for(attribute).try(:limit)
end
def model
@given_record.class
end
def failure_message_preface # rubocop:disable Metrics/MethodLength
prefix = ''
if @existing_record_created
prefix << "After taking the given #{model.name}"
if attribute_setter_for_existing_record
prefix << ', setting '
prefix << description_for_attribute_setter(
attribute_setter_for_existing_record,
)
else
prefix << ", whose :#{attribute} is "
prefix << "#{existing_value_read.inspect}"
end
prefix << ', and saving it as the existing record, then'
elsif attribute_setter_for_existing_record
prefix << "Given an existing #{model.name},"
prefix << ' after setting '
prefix << description_for_attribute_setter(
attribute_setter_for_existing_record,
)
prefix << ', then'
else
prefix << "Given an existing #{model.name} whose :#{attribute}"
prefix << ' is '
prefix << Shoulda::Matchers::Util.inspect_value(
existing_value_read,
)
prefix << ', after'
end
prefix << " making a new #{model.name} and setting "
prefix << descriptions_for_attribute_setters_for_new_record
prefix << ", the matcher expected the new #{model.name} to be"
prefix
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
)
description = "its :#{attribute_setter.attribute_name} to "
if same_as_existing == false
description << 'a different value, '
end
description << Shoulda::Matchers::Util.inspect_value(
attribute_setter.value_written,
)
if attribute_setter.attribute_changed_value?
description << ' (read back as '
description << Shoulda::Matchers::Util.inspect_value(
attribute_setter.value_read,
)
description << ')'
end
if same_as_existing == true
description << ' as well'
end
description
end
def descriptions_for_attribute_setters_for_new_record
attribute_setter_descriptions_for_new_record.to_sentence
end
def attribute_setter_descriptions_for_new_record
attribute_setters_for_new_record.map do |attribute_setter|
same_as_existing = (
attribute_setter.value_written ==
existing_value_written
)
description_for_attribute_setter(
attribute_setter,
same_as_existing: same_as_existing,
)
end
end
def existing_and_new_values_are_same?
last_value_set_on_new_record == existing_value_written
end
def last_attribute_setter_used_on_new_record
last_submatcher_run.last_attribute_setter_used
end
def last_value_set_on_new_record
last_submatcher_run.last_value_set
end
# @private
class AttributeSetters
include Enumerable
def initialize
@attribute_setters = []
end
def <<(given_attribute_setter)
index = find_index_of(given_attribute_setter)
if index
@attribute_setters[index] = given_attribute_setter
else
@attribute_setters << given_attribute_setter
end
end
def +(other_attribute_setters)
dup.tap do |attribute_setters|
other_attribute_setters.each do |attribute_setter|
attribute_setters << attribute_setter
end
end
end
def each(&block)
@attribute_setters.each(&block)
end
def last
@attribute_setters.last
end
private
def find_index_of(given_attribute_setter)
@attribute_setters.find_index do |attribute_setter|
attribute_setter.attribute_name ==
given_attribute_setter.attribute_name
end
end
end
# @private
class NonCaseSwappableValueError < Shoulda::Matchers::Error
attr_accessor :model, :attribute, :value
def message
Shoulda::Matchers.word_wrap <<-MESSAGE
Your #{model.name} model has a uniqueness validation on :#{attribute} which is
declared to be case-sensitive, but the value the uniqueness matcher used,
#{value.inspect}, doesn't contain any alpha characters, so using it to
test the case-sensitivity part of the validation is ineffective. There are
two possible solutions for this depending on what you're trying to do here:
a) If you meant for the validation to be case-sensitive, then you need to give
the uniqueness matcher a saved instance of #{model.name} with a value for
:#{attribute} that contains alpha characters.
b) If you meant for the validation to be case-insensitive, then you need to
add `case_sensitive: false` to the validation and add `case_insensitive` to
the matcher.
For more information, please see:
https://matchers.shoulda.io/docs/v#{Shoulda::Matchers::VERSION}/file.NonCaseSwappableValueError.html
MESSAGE
end
end
# @private
class ExistingRecordInvalid < Shoulda::Matchers::Error
include Shoulda::Matchers::ActiveModel::Helpers
attr_accessor :underlying_exception
def message
<<-MESSAGE.strip
validate_uniqueness_of works by matching a new record against an
existing record. If there is no existing record, it will create one
using the record you provide.
While doing this, the following error was raised:
#{Shoulda::Matchers::Util.indent(underlying_exception.message, 2)}
The best way to fix this is to provide the matcher with a record where
any required attributes are filled in with valid values beforehand.
MESSAGE
end
end
end
end
end
end