allow_value: pre-set attributes before validation
While attempting to add support for `ignoring_interference_by_writer` to the confirmation matcher, I was noticing that there are two attributes we are concerned with: the attribute under test, and the confirmation attribute -- for instance, `password` and `password_confirmation`. The way that the matcher works, `password_confirmation` is set first on the record before `password` is set, and then the whole record is validated. This is fine, but I also noticed that `allow_value` has a specific way of setting attributes -- not only does it check whether the attribute being set exists and fail properly if it is does not, but it also raises a CouldNotSetAttribute error if the attribute changes incoming values. This logic needs to be performed on both `password_confirmation` as well as `password`. With that in mind, `allow_value` now supports a `values_to_preset=` writer method which allows one to assign additional attributes unrelated to the one being tested prior to validation. This will be used by the confirmation matcher in a future commit. This means that `allow_value` now operates in two steps: 1. Set attributes unrelated to the test, raising an error if any of the attributes do not exist on the model. 2. Set the attribute under test to one or more values, raising an error if the attribute does not exist, then running validations on the record, failing with an appropriate error message if the validations fail. Note that the second step is similar to the first, although there are more things involved. To that end, `allow_value` has been completely refactored so that the logic for setting and validating attributes happens in other places. Specifically, the core logic to set an attribute (and capture the results) is located in a new AttributeSetter class. Also, the CouldNotSetAttributeError class has been moved to a namespace and renamed to AttributeChangedValueError. Finally, this commit fixes DisallowValueMatcher so that it is the true opposite of AllowValueMatcher: DVM#matches? calls AVM#does_not_match? and DVM#does_not_match? calls AVM#matches?.
This commit is contained in:
parent
6a4871547a
commit
2962112114
|
@ -3,6 +3,14 @@ require 'shoulda/matchers/active_model/validation_matcher'
|
|||
require 'shoulda/matchers/active_model/validation_matcher/build_description'
|
||||
require 'shoulda/matchers/active_model/validator'
|
||||
require 'shoulda/matchers/active_model/allow_value_matcher'
|
||||
require 'shoulda/matchers/active_model/allow_value_matcher/attribute_changed_value_error'
|
||||
require 'shoulda/matchers/active_model/allow_value_matcher/attribute_does_not_exist_error'
|
||||
require 'shoulda/matchers/active_model/allow_value_matcher/attribute_setter'
|
||||
require 'shoulda/matchers/active_model/allow_value_matcher/attribute_setter_and_validator'
|
||||
require 'shoulda/matchers/active_model/allow_value_matcher/attribute_setters'
|
||||
require 'shoulda/matchers/active_model/allow_value_matcher/attribute_setters_and_validators'
|
||||
require 'shoulda/matchers/active_model/allow_value_matcher/successful_check'
|
||||
require 'shoulda/matchers/active_model/allow_value_matcher/successful_setting'
|
||||
require 'shoulda/matchers/active_model/disallow_value_matcher'
|
||||
require 'shoulda/matchers/active_model/validate_length_of_matcher'
|
||||
require 'shoulda/matchers/active_model/validate_inclusion_of_matcher'
|
||||
|
|
|
@ -312,50 +312,17 @@ module Shoulda
|
|||
|
||||
# @private
|
||||
class AllowValueMatcher
|
||||
# @private
|
||||
class CouldNotSetAttributeError < Shoulda::Matchers::Error
|
||||
def self.create(model, attribute, expected_value, actual_value)
|
||||
super(
|
||||
model: model,
|
||||
attribute: attribute,
|
||||
expected_value: expected_value,
|
||||
actual_value: actual_value
|
||||
)
|
||||
end
|
||||
|
||||
attr_accessor :model, :attribute, :expected_value, :actual_value
|
||||
|
||||
def message
|
||||
Shoulda::Matchers.word_wrap <<-MESSAGE
|
||||
The allow_value matcher attempted to set :#{attribute} on #{model.name} to
|
||||
#{expected_value.inspect}, but when the attribute was read back, it
|
||||
had stored #{actual_value.inspect} instead.
|
||||
|
||||
This creates a problem because it means that the model is behaving in a way that
|
||||
is interfering with the test -- there's a mismatch between the test that was
|
||||
written and test that was actually run.
|
||||
|
||||
There are a couple of reasons why this could be happening:
|
||||
|
||||
* The writer method for :#{attribute} has been overridden and contains custom
|
||||
logic to prevent certain values from being set or change which values are
|
||||
stored.
|
||||
* ActiveRecord is typecasting the incoming value.
|
||||
|
||||
Regardless, the fact you're seeing this message usually indicates a larger
|
||||
problem. Please file an issue on the GitHub repo for shoulda-matchers,
|
||||
including details about your model and the test you've written, and we can point
|
||||
you in the right direction:
|
||||
|
||||
https://github.com/thoughtbot/shoulda-matchers/issues
|
||||
MESSAGE
|
||||
end
|
||||
end
|
||||
|
||||
include Helpers
|
||||
|
||||
attr_accessor :failure_message_preface
|
||||
attr_reader :last_value_set
|
||||
attr_reader(
|
||||
:after_setting_value_callback,
|
||||
:attribute_to_check_message_against,
|
||||
:attribute_to_set,
|
||||
:context,
|
||||
:instance
|
||||
)
|
||||
|
||||
attr_writer :failure_message_preface, :values_to_preset
|
||||
|
||||
def initialize(*values)
|
||||
@values_to_set = values
|
||||
|
@ -365,18 +332,13 @@ https://github.com/thoughtbot/shoulda-matchers/issues
|
|||
@expects_strict = false
|
||||
@expects_custom_validation_message = false
|
||||
@context = nil
|
||||
|
||||
@failure_message_preface = proc do
|
||||
<<-PREFIX.strip_heredoc.strip
|
||||
After setting :#{attribute_to_set} to #{last_value_set.inspect},
|
||||
the matcher expected the #{model.name} to be
|
||||
PREFIX
|
||||
end
|
||||
@values_to_preset = {}
|
||||
@failure_message_preface = nil
|
||||
end
|
||||
|
||||
def for(attribute)
|
||||
@attribute_to_set = attribute
|
||||
@attribute_to_check_message_against = attribute
|
||||
def for(attribute_name)
|
||||
@attribute_to_set = attribute_name
|
||||
@attribute_to_check_message_against = attribute_name
|
||||
self
|
||||
end
|
||||
|
||||
|
@ -402,6 +364,16 @@ https://github.com/thoughtbot/shoulda-matchers/issues
|
|||
self
|
||||
end
|
||||
|
||||
def expected_message
|
||||
if options.key?(:expected_message)
|
||||
if Symbol === options[:expected_message]
|
||||
default_expected_message
|
||||
else
|
||||
options[:expected_message]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def expects_custom_validation_message?
|
||||
@expects_custom_validation_message
|
||||
end
|
||||
|
@ -415,174 +387,193 @@ https://github.com/thoughtbot/shoulda-matchers/issues
|
|||
@expects_strict
|
||||
end
|
||||
|
||||
def ignoring_interference_by_writer
|
||||
@ignoring_interference_by_writer = true
|
||||
def ignoring_interference_by_writer(value = true)
|
||||
@ignoring_interference_by_writer = value
|
||||
self
|
||||
end
|
||||
|
||||
def ignoring_interference_by_writer?
|
||||
@ignoring_interference_by_writer
|
||||
end
|
||||
|
||||
def _after_setting_value(&callback)
|
||||
@after_setting_value_callback = callback
|
||||
end
|
||||
|
||||
def matches?(instance)
|
||||
@instance = instance
|
||||
first_failing_value_and_validator.nil?
|
||||
@result = run(:first_failing)
|
||||
@result.nil?
|
||||
end
|
||||
|
||||
def does_not_match?(instance)
|
||||
@instance = instance
|
||||
first_passing_value_and_validator.nil?
|
||||
@result = run(:first_passing)
|
||||
@result.nil?
|
||||
end
|
||||
|
||||
def failure_message
|
||||
validator = first_failing_validator
|
||||
message =
|
||||
failure_message_preface.call +
|
||||
' valid, but it was invalid instead,'
|
||||
attribute_setter = result.attribute_setter
|
||||
|
||||
if validator.captured_validation_exception?
|
||||
message << ' raising a validation exception with the message '
|
||||
message << validator.validation_exception_message.inspect
|
||||
message << '.'
|
||||
if result.attribute_setter.unsuccessfully_checked?
|
||||
message = attribute_setter.failure_message
|
||||
else
|
||||
message << " producing these validation errors:\n\n"
|
||||
message << validator.all_formatted_validation_error_messages
|
||||
validator = result.validator
|
||||
message = failure_message_preface.call
|
||||
message << ' valid, but it was invalid instead,'
|
||||
|
||||
if validator.captured_validation_exception?
|
||||
message << ' raising a validation exception with the message '
|
||||
message << validator.validation_exception_message.inspect
|
||||
message << '.'
|
||||
else
|
||||
message << " producing these validation errors:\n\n"
|
||||
message << validator.all_formatted_validation_error_messages
|
||||
end
|
||||
end
|
||||
|
||||
Shoulda::Matchers.word_wrap(message)
|
||||
end
|
||||
|
||||
def failure_message_when_negated
|
||||
validator = first_passing_validator
|
||||
message = failure_message_preface.call + ' invalid'
|
||||
attribute_setter = result.attribute_setter
|
||||
|
||||
if validator.type_of_message_matched?
|
||||
if validator.has_messages?
|
||||
message << ' and to'
|
||||
if attribute_setter.unsuccessfully_checked?
|
||||
message = attribute_setter.failure_message
|
||||
else
|
||||
validator = result.validator
|
||||
message = failure_message_preface.call + ' invalid'
|
||||
|
||||
if validator.captured_validation_exception?
|
||||
message << ' raise a validation exception with message'
|
||||
else
|
||||
message << ' produce'
|
||||
if validator.type_of_message_matched?
|
||||
if validator.has_messages?
|
||||
message << ' and to'
|
||||
|
||||
if expected_message.is_a?(Regexp)
|
||||
message << ' a'
|
||||
if validator.captured_validation_exception?
|
||||
message << ' raise a validation exception with message'
|
||||
else
|
||||
message << ' the'
|
||||
message << ' produce'
|
||||
|
||||
if expected_message.is_a?(Regexp)
|
||||
message << ' a'
|
||||
else
|
||||
message << ' the'
|
||||
end
|
||||
|
||||
message << ' validation error'
|
||||
end
|
||||
|
||||
message << ' validation error'
|
||||
end
|
||||
if expected_message.is_a?(Regexp)
|
||||
message << ' matching'
|
||||
end
|
||||
|
||||
if expected_message.is_a?(Regexp)
|
||||
message << ' matching'
|
||||
end
|
||||
message << " #{expected_message.inspect}"
|
||||
|
||||
message << " #{expected_message.inspect}"
|
||||
unless validator.captured_validation_exception?
|
||||
message << " on :#{attribute_to_check_message_against}"
|
||||
end
|
||||
|
||||
unless validator.captured_validation_exception?
|
||||
message << " on :#{attribute_to_check_message_against}"
|
||||
end
|
||||
message << '. The record was indeed invalid, but'
|
||||
|
||||
message << '. The record was indeed invalid, but'
|
||||
|
||||
if validator.captured_validation_exception?
|
||||
message << ' the exception message was '
|
||||
message << validator.validation_exception_message.inspect
|
||||
message << ' instead.'
|
||||
if validator.captured_validation_exception?
|
||||
message << ' the exception message was '
|
||||
message << validator.validation_exception_message.inspect
|
||||
message << ' instead.'
|
||||
else
|
||||
message << " it produced these validation errors instead:\n\n"
|
||||
message << validator.all_formatted_validation_error_messages
|
||||
end
|
||||
else
|
||||
message << " it produced these validation errors instead:\n\n"
|
||||
message << validator.all_formatted_validation_error_messages
|
||||
message << ', but it was valid instead.'
|
||||
end
|
||||
elsif validator.captured_validation_exception?
|
||||
message << ' and to produce validation errors, but the record'
|
||||
message << ' raised a validation exception instead.'
|
||||
else
|
||||
message << ', but it was valid instead.'
|
||||
message << ' and to raise a validation exception, but the record'
|
||||
message << ' produced validation errors instead.'
|
||||
end
|
||||
elsif validator.captured_validation_exception?
|
||||
message << ' and to produce validation errors, but the record'
|
||||
message << ' raised a validation exception instead.'
|
||||
else
|
||||
message << ' and to raise a validation exception, but the record'
|
||||
message << ' produced validation errors instead.'
|
||||
end
|
||||
|
||||
Shoulda::Matchers.word_wrap(message)
|
||||
end
|
||||
|
||||
def simple_description
|
||||
"allow :#{attribute_to_set} to be #{inspected_values_to_set}"
|
||||
end
|
||||
|
||||
def description
|
||||
ValidationMatcher::BuildDescription.call(self, simple_description)
|
||||
end
|
||||
|
||||
def simple_description
|
||||
"allow :#{attribute_to_set} to be #{inspected_values_to_set}"
|
||||
end
|
||||
|
||||
def model
|
||||
instance.class
|
||||
end
|
||||
|
||||
def last_value_set
|
||||
result.attribute_setter.value_written
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader(
|
||||
:after_setting_value_callback,
|
||||
:attribute_to_check_message_against,
|
||||
:attribute_to_set,
|
||||
:context,
|
||||
:instance,
|
||||
:options,
|
||||
:result,
|
||||
:values_to_preset,
|
||||
:values_to_set,
|
||||
)
|
||||
|
||||
private
|
||||
|
||||
def ignoring_interference_by_writer?
|
||||
@ignoring_interference_by_writer
|
||||
def run(strategy)
|
||||
attribute_setters_for_values_to_preset.first_failing ||
|
||||
attribute_setters_and_validators_for_values_to_set.public_send(strategy)
|
||||
end
|
||||
|
||||
def value_matches?(value, validator)
|
||||
@last_value_set = value
|
||||
set_attribute(value)
|
||||
!errors_match?(validator) && !any_range_error_occurred?(validator)
|
||||
def failure_message_preface
|
||||
@failure_message_preface || method(:default_failure_message_preface)
|
||||
end
|
||||
|
||||
def set_attribute(value)
|
||||
instance.__send__("#{attribute_to_set}=", value)
|
||||
ensure_that_attribute_was_set!(value)
|
||||
after_setting_value_callback.call
|
||||
def default_failure_message_preface
|
||||
''.tap do |preface|
|
||||
if descriptions_for_preset_values.any?
|
||||
preface << 'After setting '
|
||||
preface << descriptions_for_preset_values.to_sentence
|
||||
preface << ', then '
|
||||
else
|
||||
preface << 'After '
|
||||
end
|
||||
|
||||
preface << 'setting '
|
||||
preface << description_for_resulting_attribute_setter
|
||||
|
||||
unless preface.end_with?('--')
|
||||
preface << ','
|
||||
end
|
||||
|
||||
preface << " the matcher expected the #{model.name} to be"
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_that_attribute_was_set!(expected_value)
|
||||
actual_value = instance.__send__(attribute_to_set)
|
||||
def descriptions_for_preset_values
|
||||
attribute_setters_for_values_to_preset.
|
||||
map(&:attribute_setter_description)
|
||||
end
|
||||
|
||||
if expected_value != actual_value && !ignoring_interference_by_writer?
|
||||
raise CouldNotSetAttributeError.create(
|
||||
instance.class,
|
||||
attribute_to_set,
|
||||
expected_value,
|
||||
actual_value
|
||||
def description_for_resulting_attribute_setter
|
||||
result.attribute_setter_description
|
||||
end
|
||||
|
||||
def attribute_setters_for_values_to_preset
|
||||
@_attribute_setters_for_values_to_preset ||=
|
||||
AttributeSetters.new(self, values_to_preset)
|
||||
end
|
||||
|
||||
def attribute_setters_and_validators_for_values_to_set
|
||||
@_attribute_setters_and_validators_for_values_to_set ||=
|
||||
AttributeSettersAndValidators.new(
|
||||
self,
|
||||
values_to_set.map { |value| [attribute_to_set, value] }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def errors_match?(validator)
|
||||
validator.has_messages? &&
|
||||
validator.type_of_message_matched? &&
|
||||
errors_for_attribute_match?(validator)
|
||||
end
|
||||
|
||||
def errors_for_attribute_match?(validator)
|
||||
matched_errors(validator).compact.any?
|
||||
end
|
||||
|
||||
def matched_errors(validator)
|
||||
if expected_message
|
||||
validator.messages.grep(expected_message)
|
||||
else
|
||||
validator.messages
|
||||
end
|
||||
end
|
||||
|
||||
def any_range_error_occurred?(validator)
|
||||
validator.captured_range_error?
|
||||
end
|
||||
|
||||
def inspected_values_to_set
|
||||
|
@ -596,16 +587,6 @@ https://github.com/thoughtbot/shoulda-matchers/issues
|
|||
end
|
||||
end
|
||||
|
||||
def expected_message
|
||||
if options.key?(:expected_message)
|
||||
if Symbol === options[:expected_message]
|
||||
default_expected_message
|
||||
else
|
||||
options[:expected_message]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def default_expected_message
|
||||
if expects_strict?
|
||||
"#{human_attribute_name} #{default_attribute_message}"
|
||||
|
@ -631,50 +612,14 @@ https://github.com/thoughtbot/shoulda-matchers/issues
|
|||
defaults.merge(options[:expected_message_values])
|
||||
end
|
||||
|
||||
def values_and_validators
|
||||
@_values_and_validators ||= values_to_set.map do |value|
|
||||
[value, build_validator]
|
||||
end
|
||||
end
|
||||
|
||||
def build_validator
|
||||
validator = Validator.new(
|
||||
instance,
|
||||
attribute_to_check_message_against
|
||||
)
|
||||
validator.context = context
|
||||
validator.expects_strict = expects_strict?
|
||||
validator
|
||||
end
|
||||
|
||||
def first_failing_value_and_validator
|
||||
@_first_failing_value_and_validator ||=
|
||||
values_and_validators.detect do |value, validator|
|
||||
!value_matches?(value, validator)
|
||||
end
|
||||
end
|
||||
|
||||
def first_failing_validator
|
||||
first_failing_value_and_validator[1]
|
||||
end
|
||||
|
||||
def first_passing_value_and_validator
|
||||
@_first_passing_value_and_validator ||=
|
||||
values_and_validators.detect do |value, validator|
|
||||
value_matches?(value, validator)
|
||||
end
|
||||
end
|
||||
|
||||
def first_passing_validator
|
||||
first_passing_value_and_validator[1]
|
||||
end
|
||||
|
||||
def model_name
|
||||
instance.class.to_s.underscore
|
||||
end
|
||||
|
||||
def human_attribute_name
|
||||
instance.class.human_attribute_name(attribute_to_check_message_against)
|
||||
instance.class.human_attribute_name(
|
||||
attribute_to_check_message_against
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
module Shoulda
|
||||
module Matchers
|
||||
module ActiveModel
|
||||
class AllowValueMatcher
|
||||
# @private
|
||||
class AttributeChangedValueError < Shoulda::Matchers::Error
|
||||
attr_accessor :matcher_name, :model, :attribute_name, :value_written,
|
||||
:value_read
|
||||
|
||||
def message
|
||||
Shoulda::Matchers.word_wrap <<-MESSAGE
|
||||
The #{matcher_name} matcher attempted to set :#{attribute_name} on
|
||||
#{model.name} to #{value_written.inspect}, but when the attribute was
|
||||
read back, it had stored #{value_read.inspect} instead.
|
||||
|
||||
This creates a problem because it means that the model is behaving in a way that
|
||||
is interfering with the test -- there's a mismatch between the test that was
|
||||
written and test that was actually run.
|
||||
|
||||
There are a couple of reasons why this could be happening:
|
||||
|
||||
* The writer method for :#{attribute_name} has been overridden and contains
|
||||
custom logic to prevent certain values from being set or change which values
|
||||
are stored.
|
||||
* ActiveRecord is typecasting the incoming value.
|
||||
|
||||
Regardless, the fact you're seeing this message usually indicates a larger
|
||||
problem. Please file an issue on the GitHub repo for shoulda-matchers,
|
||||
including details about your model and the test you've written, and we can point
|
||||
you in the right direction:
|
||||
|
||||
https://github.com/thoughtbot/shoulda-matchers/issues
|
||||
MESSAGE
|
||||
end
|
||||
|
||||
def successful?
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
module Shoulda
|
||||
module Matchers
|
||||
module ActiveModel
|
||||
class AllowValueMatcher
|
||||
# @private
|
||||
class AttributeDoesNotExistError < Shoulda::Matchers::Error
|
||||
attr_accessor :model, :attribute_name, :value
|
||||
|
||||
def message
|
||||
Shoulda::Matchers.word_wrap <<-MESSAGE
|
||||
The matcher attempted to set :#{attribute_name} to #{value.inspect} on
|
||||
the #{model.name}, but that attribute does not exist.
|
||||
MESSAGE
|
||||
end
|
||||
|
||||
def successful?
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,197 @@
|
|||
module Shoulda
|
||||
module Matchers
|
||||
module ActiveModel
|
||||
class AllowValueMatcher
|
||||
# @private
|
||||
class AttributeSetter
|
||||
def self.set(args)
|
||||
new(args).set
|
||||
end
|
||||
|
||||
attr_reader :result_of_checking, :result_of_setting,
|
||||
:value_written
|
||||
|
||||
def initialize(args)
|
||||
@matcher_name = args.fetch(:matcher_name)
|
||||
@object = args.fetch(:object)
|
||||
@attribute_name = args.fetch(:attribute_name)
|
||||
@value_written = args.fetch(:value)
|
||||
@ignoring_interference_by_writer =
|
||||
args.fetch(:ignoring_interference_by_writer, false)
|
||||
@after_set_callback = args.fetch(:after_set_callback, -> { })
|
||||
|
||||
@result_of_checking = nil
|
||||
@result_of_setting = nil
|
||||
end
|
||||
|
||||
def description
|
||||
description = ":#{attribute_name} to #{value_written.inspect}"
|
||||
|
||||
if attribute_changed_value?
|
||||
description << " -- which was read back as "
|
||||
description << "#{value_read.inspect} --"
|
||||
end
|
||||
|
||||
description
|
||||
end
|
||||
|
||||
def run
|
||||
check && set
|
||||
end
|
||||
|
||||
def run!
|
||||
check && set!
|
||||
end
|
||||
|
||||
def check
|
||||
if attribute_exists?
|
||||
@result_of_checking = successful_check
|
||||
true
|
||||
else
|
||||
@result_of_checking = attribute_does_not_exist_error
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def set!
|
||||
if attribute_exists?
|
||||
set
|
||||
|
||||
unless result_of_setting.successful?
|
||||
raise result_of_setting
|
||||
end
|
||||
|
||||
@result_of_checking = successful_check
|
||||
@result_of_setting = successful_setting
|
||||
|
||||
true
|
||||
else
|
||||
attribute_does_not_exist!
|
||||
end
|
||||
end
|
||||
|
||||
def set
|
||||
object.public_send("#{attribute_name}=", value_written)
|
||||
after_set_callback.call
|
||||
|
||||
@result_of_checking = successful_check
|
||||
|
||||
if attribute_changed_value? && !ignoring_interference_by_writer?
|
||||
@result_of_setting = attribute_changed_value_error
|
||||
false
|
||||
else
|
||||
@result_of_setting = successful_setting
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def failure_message
|
||||
if successful?
|
||||
raise "We're not supposed to be here!"
|
||||
elsif result_of_setting
|
||||
result_of_setting.message
|
||||
else
|
||||
result_of_checking.message
|
||||
end
|
||||
end
|
||||
|
||||
def successful?
|
||||
successfully_checked? && successfully_set?
|
||||
end
|
||||
|
||||
def unsuccessful?
|
||||
!successful?
|
||||
end
|
||||
|
||||
def checked?
|
||||
!result_of_checking.nil?
|
||||
end
|
||||
|
||||
def successfully_checked?
|
||||
checked? && result_of_checking.successful?
|
||||
end
|
||||
|
||||
def unsuccessfully_checked?
|
||||
!successfully_checked?
|
||||
end
|
||||
|
||||
def set?
|
||||
!result_of_setting.nil?
|
||||
end
|
||||
|
||||
def successfully_set?
|
||||
set? && result_of_setting.successful?
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :matcher_name, :object, :attribute_name,
|
||||
:after_set_callback
|
||||
|
||||
private
|
||||
|
||||
def model
|
||||
object.class
|
||||
end
|
||||
|
||||
def attribute_exists?
|
||||
if active_resource_object?
|
||||
object.known_attributes.include?(attribute_name.to_s)
|
||||
else
|
||||
object.respond_to?("#{attribute_name}=")
|
||||
end
|
||||
end
|
||||
|
||||
def attribute_changed_value?
|
||||
value_written != value_read
|
||||
end
|
||||
|
||||
def value_read
|
||||
@_value_read ||= object.public_send(attribute_name)
|
||||
end
|
||||
|
||||
def ignoring_interference_by_writer?
|
||||
!!@ignoring_interference_by_writer
|
||||
end
|
||||
|
||||
def successful_check
|
||||
SuccessfulCheck.new
|
||||
end
|
||||
|
||||
def successful_setting
|
||||
SuccessfulSetting.new
|
||||
end
|
||||
|
||||
def attribute_changed_value!
|
||||
raise attribute_changed_value_error
|
||||
end
|
||||
|
||||
def attribute_changed_value_error
|
||||
AttributeChangedValueError.create(
|
||||
model: object.class,
|
||||
attribute_name: attribute_name,
|
||||
value_written: value_written,
|
||||
value_read: value_read
|
||||
)
|
||||
end
|
||||
|
||||
def attribute_does_not_exist!
|
||||
raise attribute_does_not_exist_error
|
||||
end
|
||||
|
||||
def attribute_does_not_exist_error
|
||||
AttributeDoesNotExistError.create(
|
||||
model: object.class,
|
||||
attribute_name: attribute_name,
|
||||
value: value_written
|
||||
)
|
||||
end
|
||||
|
||||
def active_resource_object?
|
||||
object.respond_to?(:known_attributes)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,62 @@
|
|||
require 'forwardable'
|
||||
|
||||
module Shoulda
|
||||
module Matchers
|
||||
module ActiveModel
|
||||
class AllowValueMatcher
|
||||
# @private
|
||||
class AttributeSetterAndValidator
|
||||
extend Forwardable
|
||||
|
||||
def_delegators(
|
||||
:allow_value_matcher,
|
||||
:after_setting_value_callback,
|
||||
:attribute_to_check_message_against,
|
||||
:context,
|
||||
:expected_message,
|
||||
:expects_strict?,
|
||||
:ignoring_interference_by_writer?,
|
||||
:instance,
|
||||
)
|
||||
|
||||
def initialize(allow_value_matcher, attribute_name, value)
|
||||
@allow_value_matcher = allow_value_matcher
|
||||
@attribute_name = attribute_name
|
||||
@value = value
|
||||
@_attribute_setter = nil
|
||||
@_validator = nil
|
||||
end
|
||||
|
||||
def attribute_setter
|
||||
@_attribute_setter ||= AttributeSetter.new(
|
||||
matcher_name: :allow_value,
|
||||
object: instance,
|
||||
attribute_name: attribute_name,
|
||||
value: value,
|
||||
ignoring_interference_by_writer: ignoring_interference_by_writer?,
|
||||
after_set_callback: after_setting_value_callback
|
||||
)
|
||||
end
|
||||
|
||||
def attribute_setter_description
|
||||
attribute_setter.description
|
||||
end
|
||||
|
||||
def validator
|
||||
@_validator ||= Validator.new(
|
||||
instance,
|
||||
attribute_to_check_message_against,
|
||||
context: context,
|
||||
expects_strict: expects_strict?,
|
||||
expected_message: expected_message
|
||||
)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :allow_value_matcher, :attribute_name, :value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,40 @@
|
|||
module Shoulda
|
||||
module Matchers
|
||||
module ActiveModel
|
||||
class AllowValueMatcher
|
||||
# @private
|
||||
class AttributeSetters
|
||||
include Enumerable
|
||||
|
||||
def initialize(allow_value_matcher, values)
|
||||
@tuples = values.map do |attribute_name, value|
|
||||
AttributeSetterAndValidator.new(
|
||||
allow_value_matcher,
|
||||
attribute_name,
|
||||
value
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
tuples.each(&block)
|
||||
end
|
||||
|
||||
def first_failing
|
||||
tuples.detect(&method(:does_not_match?))
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :tuples
|
||||
|
||||
private
|
||||
|
||||
def does_not_match?(tuple)
|
||||
!tuple.attribute_setter.set!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,48 @@
|
|||
module Shoulda
|
||||
module Matchers
|
||||
module ActiveModel
|
||||
class AllowValueMatcher
|
||||
# @private
|
||||
class AttributeSettersAndValidators
|
||||
include Enumerable
|
||||
|
||||
def initialize(allow_value_matcher, values)
|
||||
@tuples = values.map do |attribute_name, value|
|
||||
AttributeSetterAndValidator.new(
|
||||
allow_value_matcher,
|
||||
attribute_name,
|
||||
value
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
tuples.each(&block)
|
||||
end
|
||||
|
||||
def first_passing
|
||||
tuples.detect(&method(:matches?))
|
||||
end
|
||||
|
||||
def first_failing
|
||||
tuples.detect(&method(:does_not_match?))
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :tuples
|
||||
|
||||
private
|
||||
|
||||
def matches?(tuple)
|
||||
tuple.attribute_setter.set! && tuple.validator.call
|
||||
end
|
||||
|
||||
def does_not_match?(tuple)
|
||||
!matches?(tuple)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,14 @@
|
|||
module Shoulda
|
||||
module Matchers
|
||||
module ActiveModel
|
||||
class AllowValueMatcher
|
||||
# @private
|
||||
class SuccessfulCheck
|
||||
def successful?
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,14 @@
|
|||
module Shoulda
|
||||
module Matchers
|
||||
module ActiveModel
|
||||
class AllowValueMatcher
|
||||
# @private
|
||||
class SuccessfulSetting
|
||||
def successful?
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -15,14 +15,19 @@ module Shoulda
|
|||
:last_value_set,
|
||||
:simple_description,
|
||||
:failure_message_preface,
|
||||
:failure_message_preface=
|
||||
:failure_message_preface=,
|
||||
:values_to_preset=
|
||||
|
||||
def initialize(value)
|
||||
@allow_matcher = AllowValueMatcher.new(value)
|
||||
end
|
||||
|
||||
def matches?(subject)
|
||||
!allow_matcher.matches?(subject)
|
||||
allow_matcher.does_not_match?(subject)
|
||||
end
|
||||
|
||||
def does_not_match?(subject)
|
||||
allow_matcher.matches?(subject)
|
||||
end
|
||||
|
||||
def for(attribute)
|
||||
|
|
|
@ -140,14 +140,14 @@ module Shoulda
|
|||
|
||||
def disallows_and_double_checks_value_of!(value, message)
|
||||
disallows_value_of(value, message)
|
||||
rescue ActiveModel::AllowValueMatcher::CouldNotSetAttributeError
|
||||
rescue ActiveModel::AllowValueMatcher::AttributeChangedValueError
|
||||
raise ActiveModel::CouldNotSetPasswordError.create(@subject.class)
|
||||
end
|
||||
|
||||
def disallows_original_or_typecast_value?(value, message)
|
||||
disallows_value_of(blank_value, @expected_message)
|
||||
rescue ActiveModel::AllowValueMatcher::CouldNotSetAttributeError => error
|
||||
error.actual_value.blank?
|
||||
rescue ActiveModel::AllowValueMatcher::AttributeChangedValueError => error
|
||||
error.value_read.blank?
|
||||
end
|
||||
|
||||
def blank_value
|
||||
|
|
|
@ -5,58 +5,38 @@ module Shoulda
|
|||
class Validator
|
||||
include Helpers
|
||||
|
||||
attr_writer :context, :expects_strict
|
||||
|
||||
def initialize(record, attribute)
|
||||
def initialize(record, attribute, options = {})
|
||||
@record = record
|
||||
@attribute = attribute
|
||||
@context = context
|
||||
@expects_strict = false
|
||||
reset
|
||||
end
|
||||
@context = options[:context]
|
||||
@expects_strict = options[:expects_strict]
|
||||
@expected_message = options[:expected_message]
|
||||
|
||||
def reset
|
||||
@_validation_result = nil
|
||||
@captured_validation_exception = false
|
||||
@captured_range_error = false
|
||||
end
|
||||
|
||||
def messages
|
||||
if expects_strict?
|
||||
[validation_exception_message]
|
||||
else
|
||||
validation_error_messages
|
||||
end
|
||||
def call
|
||||
!messages_match? && !captured_range_error?
|
||||
end
|
||||
|
||||
def has_messages?
|
||||
messages.any?
|
||||
end
|
||||
|
||||
def type_of_message_matched?
|
||||
expects_strict? == captured_validation_exception?
|
||||
end
|
||||
|
||||
def captured_validation_exception?
|
||||
@captured_validation_exception
|
||||
end
|
||||
|
||||
def captured_range_error?
|
||||
!!@captured_range_error
|
||||
end
|
||||
|
||||
def all_validation_errors
|
||||
validation_result[:all_validation_errors]
|
||||
def type_of_message_matched?
|
||||
expects_strict? == captured_validation_exception?
|
||||
end
|
||||
|
||||
def all_formatted_validation_error_messages
|
||||
format_validation_errors(all_validation_errors)
|
||||
end
|
||||
|
||||
def validation_error_messages
|
||||
validation_result[:validation_error_messages]
|
||||
end
|
||||
|
||||
def validation_exception_message
|
||||
validation_result[:validation_exception_message]
|
||||
end
|
||||
|
@ -71,6 +51,40 @@ module Shoulda
|
|||
@expects_strict
|
||||
end
|
||||
|
||||
def messages_match?
|
||||
has_messages? &&
|
||||
type_of_message_matched? &&
|
||||
matched_messages.compact.any?
|
||||
end
|
||||
|
||||
def messages
|
||||
if expects_strict?
|
||||
[validation_exception_message]
|
||||
else
|
||||
validation_error_messages
|
||||
end
|
||||
end
|
||||
|
||||
def matched_messages
|
||||
if @expected_message
|
||||
messages.grep(@expected_message)
|
||||
else
|
||||
messages
|
||||
end
|
||||
end
|
||||
|
||||
def captured_range_error?
|
||||
!!@captured_range_error
|
||||
end
|
||||
|
||||
def all_validation_errors
|
||||
validation_result[:all_validation_errors]
|
||||
end
|
||||
|
||||
def validation_error_messages
|
||||
validation_result[:validation_error_messages]
|
||||
end
|
||||
|
||||
def validation_result
|
||||
@_validation_result ||= perform_validation
|
||||
end
|
||||
|
|
|
@ -412,7 +412,7 @@ invalid" instead.
|
|||
context 'when the attribute interferes with attempts to be set' do
|
||||
context 'when the matcher has not been qualified with #ignoring_interference_by_writer' do
|
||||
context 'when the attribute cannot be changed from nil to non-nil' do
|
||||
it 'raises a CouldNotSetAttributeError' do
|
||||
it 'raises an AttributeChangedValueError' do
|
||||
model = define_active_model_class 'Example' do
|
||||
attr_reader :name
|
||||
|
||||
|
@ -426,13 +426,13 @@ invalid" instead.
|
|||
}
|
||||
|
||||
expect(&assertion).to raise_error(
|
||||
described_class::CouldNotSetAttributeError
|
||||
described_class::AttributeChangedValueError
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the attribute cannot be changed from non-nil to nil' do
|
||||
it 'raises a CouldNotSetAttribute error' do
|
||||
it 'raises an AttributeChangedValueError' do
|
||||
model = define_active_model_class 'Example' do
|
||||
attr_reader :name
|
||||
|
||||
|
@ -448,13 +448,13 @@ invalid" instead.
|
|||
}
|
||||
|
||||
expect(&assertion).to raise_error(
|
||||
described_class::CouldNotSetAttributeError
|
||||
described_class::AttributeChangedValueError
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the attribute cannot be changed from a non-nil value to another non-nil value' do
|
||||
it 'raises a CouldNotSetAttribute error' do
|
||||
it 'raises an AttributeChangedValueError' do
|
||||
model = define_active_model_class 'Example' do
|
||||
attr_reader :name
|
||||
|
||||
|
@ -470,7 +470,7 @@ invalid" instead.
|
|||
}
|
||||
|
||||
expect(&assertion).to raise_error(
|
||||
described_class::CouldNotSetAttributeError
|
||||
described_class::AttributeChangedValueError
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -545,4 +545,100 @@ invalid" instead.
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the attribute does not exist on the model' do
|
||||
context 'when the assertion is positive' do
|
||||
it 'raises an AttributeDoesNotExistError' do
|
||||
model = define_class('Example')
|
||||
|
||||
assertion = lambda do
|
||||
expect(model.new).to allow_value('foo').for(:nonexistent)
|
||||
end
|
||||
|
||||
message = <<-MESSAGE.rstrip
|
||||
The matcher attempted to set :nonexistent to "foo" on the Example, but
|
||||
that attribute does not exist.
|
||||
MESSAGE
|
||||
|
||||
expect(&assertion).to raise_error(
|
||||
described_class::AttributeDoesNotExistError,
|
||||
message
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the assertion is negative' do
|
||||
it 'raises an AttributeDoesNotExistError' do
|
||||
model = define_class('Example')
|
||||
|
||||
assertion = lambda do
|
||||
expect(model.new).not_to allow_value('foo').for(:nonexistent)
|
||||
end
|
||||
|
||||
message = <<-MESSAGE.rstrip
|
||||
The matcher attempted to set :nonexistent to "foo" on the Example, but
|
||||
that attribute does not exist.
|
||||
MESSAGE
|
||||
|
||||
expect(&assertion).to raise_error(
|
||||
described_class::AttributeDoesNotExistError,
|
||||
message
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'given attributes to preset on the record before validation' do
|
||||
context 'when the assertion is positive' do
|
||||
context 'if any attributes do not exist on the model' do
|
||||
it 'raises an AttributeDoesNotExistError' do
|
||||
model = define_active_model_class('Example', accessors: [:existent])
|
||||
|
||||
allow_value_matcher = allow_value('foo').for(:existent).tap do |matcher|
|
||||
matcher.values_to_preset = { nonexistent: 'some value' }
|
||||
end
|
||||
|
||||
assertion = lambda do
|
||||
expect(model.new).to(allow_value_matcher)
|
||||
end
|
||||
|
||||
message = <<-MESSAGE.rstrip
|
||||
The matcher attempted to set :nonexistent to "some value" on the
|
||||
Example, but that attribute does not exist.
|
||||
MESSAGE
|
||||
|
||||
expect(&assertion).to raise_error(
|
||||
described_class::AttributeDoesNotExistError,
|
||||
message
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the assertion is negative' do
|
||||
context 'if any attributes do not exist on the model' do
|
||||
it 'raises an AttributeDoesNotExistError' do
|
||||
model = define_active_model_class('Example', accessors: [:existent])
|
||||
|
||||
allow_value_matcher = allow_value('foo').for(:existent).tap do |matcher|
|
||||
matcher.values_to_preset = { nonexistent: 'some value' }
|
||||
end
|
||||
|
||||
assertion = lambda do
|
||||
expect(model.new).not_to(allow_value_matcher)
|
||||
end
|
||||
|
||||
message = <<-MESSAGE.rstrip
|
||||
The matcher attempted to set :nonexistent to "some value" on the
|
||||
Example, but that attribute does not exist.
|
||||
MESSAGE
|
||||
|
||||
expect(&assertion).to raise_error(
|
||||
described_class::AttributeDoesNotExistError,
|
||||
message
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -184,7 +184,7 @@ raising a validation exception on failure.
|
|||
|
||||
if rails_4_x?
|
||||
context 'against a pre-set password in a model that has_secure_password' do
|
||||
it 'raises a CouldNotSetPasswordError exception' do
|
||||
it 'raises a CouldNotSetPasswordError' do
|
||||
user_class = define_model :user, password_digest: :string do
|
||||
has_secure_password validations: false
|
||||
validates_presence_of :password
|
||||
|
@ -193,10 +193,13 @@ raising a validation exception on failure.
|
|||
user = user_class.new
|
||||
user.password = 'something'
|
||||
|
||||
error_class = Shoulda::Matchers::ActiveModel::CouldNotSetPasswordError
|
||||
expect do
|
||||
assertion = lambda do
|
||||
expect(user).to validate_presence_of(:password)
|
||||
end.to raise_error(error_class)
|
||||
end
|
||||
|
||||
expect(&assertion).to raise_error(
|
||||
Shoulda::Matchers::ActiveModel::CouldNotSetPasswordError
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue