mirror of
https://github.com/thoughtbot/shoulda-matchers.git
synced 2022-11-09 12:01:38 -05:00
Refactor allow_value & disallow_value
This is part of a collection of commits that aim to improve failure messages across the board, in order to make matchers easier to debug when something goes wrong. * Have the failure message describe more clearly what the `allow_value` matcher was trying to do when it failed. * Make the description of the matcher more readable. * For each value that `allow_value` sets, use a different Validator instance. The matcher still changes state as it runs, but a future commit will refactor this further. * Merge StrictValidator back into Validator, and remove it. The way that StrictValidator worked (as a module that was mixed into an instance of Validator at runtime) was confusing, and there's really no need to split out the logic anymore. * Fix or fill in tests involving failure messages and descriptions.
This commit is contained in:
parent
8361f39579
commit
583be384c3
7 changed files with 517 additions and 290 deletions
|
@ -2,7 +2,6 @@ require 'shoulda/matchers/active_model/helpers'
|
||||||
require 'shoulda/matchers/active_model/validation_matcher'
|
require 'shoulda/matchers/active_model/validation_matcher'
|
||||||
require 'shoulda/matchers/active_model/validation_matcher/build_description'
|
require 'shoulda/matchers/active_model/validation_matcher/build_description'
|
||||||
require 'shoulda/matchers/active_model/validator'
|
require 'shoulda/matchers/active_model/validator'
|
||||||
require 'shoulda/matchers/active_model/strict_validator'
|
|
||||||
require 'shoulda/matchers/active_model/allow_value_matcher'
|
require 'shoulda/matchers/active_model/allow_value_matcher'
|
||||||
require 'shoulda/matchers/active_model/disallow_value_matcher'
|
require 'shoulda/matchers/active_model/disallow_value_matcher'
|
||||||
require 'shoulda/matchers/active_model/validate_length_of_matcher'
|
require 'shoulda/matchers/active_model/validate_length_of_matcher'
|
||||||
|
|
|
@ -354,99 +354,194 @@ https://github.com/thoughtbot/shoulda-matchers/issues
|
||||||
|
|
||||||
include Helpers
|
include Helpers
|
||||||
|
|
||||||
attr_accessor :attribute_with_message
|
attr_accessor :failure_message_preface
|
||||||
attr_accessor :options
|
attr_reader :last_value_set
|
||||||
|
|
||||||
def initialize(*values)
|
def initialize(*values)
|
||||||
self.values_to_match = values
|
@values_to_set = values
|
||||||
self.options = {}
|
@options = {}
|
||||||
self.after_setting_value_callback = -> {}
|
@after_setting_value_callback = -> {}
|
||||||
self.validator = Validator.new
|
|
||||||
@ignoring_interference_by_writer = false
|
@ignoring_interference_by_writer = false
|
||||||
|
@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
|
||||||
end
|
end
|
||||||
|
|
||||||
def for(attribute)
|
def for(attribute)
|
||||||
self.attribute_to_set = attribute
|
@attribute_to_set = attribute
|
||||||
self.attribute_to_check_message_against = attribute
|
@attribute_to_check_message_against = attribute
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
def on(context)
|
def on(context)
|
||||||
validator.context = context
|
if context.present?
|
||||||
self
|
@context = context
|
||||||
end
|
|
||||||
|
|
||||||
def with_message(message, options={})
|
|
||||||
self.options[:expected_message] = message
|
|
||||||
self.options[:expected_message_values] = options.fetch(:values, {})
|
|
||||||
|
|
||||||
if options.key?(:against)
|
|
||||||
self.attribute_to_check_message_against = options[:against]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
def strict
|
def with_message(message, given_options = {})
|
||||||
validator.strict = true
|
if message.present?
|
||||||
|
@expects_custom_validation_message = true
|
||||||
|
options[:expected_message] = message
|
||||||
|
options[:expected_message_values] = given_options.fetch(:values, {})
|
||||||
|
|
||||||
|
if given_options.key?(:against)
|
||||||
|
@attribute_to_check_message_against = given_options[:against]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def expects_custom_validation_message?
|
||||||
|
@expects_custom_validation_message
|
||||||
|
end
|
||||||
|
|
||||||
|
def strict(expects_strict = true)
|
||||||
|
@expects_strict = expects_strict
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def expects_strict?
|
||||||
|
@expects_strict
|
||||||
|
end
|
||||||
|
|
||||||
def ignoring_interference_by_writer
|
def ignoring_interference_by_writer
|
||||||
@ignoring_interference_by_writer = true
|
@ignoring_interference_by_writer = true
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
def _after_setting_value(&callback)
|
def _after_setting_value(&callback)
|
||||||
self.after_setting_value_callback = callback
|
@after_setting_value_callback = callback
|
||||||
end
|
end
|
||||||
|
|
||||||
def matches?(instance)
|
def matches?(instance)
|
||||||
self.instance = instance
|
@instance = instance
|
||||||
values_to_match.all? { |value| value_matches?(value) }
|
first_failing_value_and_validator.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
def does_not_match?(instance)
|
def does_not_match?(instance)
|
||||||
self.instance = instance
|
@instance = instance
|
||||||
values_to_match.all? { |value| !value_matches?(value) }
|
first_passing_value_and_validator.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
def failure_message
|
def failure_message
|
||||||
"Did not expect #{expectation},\ngot#{error_description}"
|
validator = first_failing_validator
|
||||||
|
message =
|
||||||
|
failure_message_preface.call +
|
||||||
|
' 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
|
||||||
|
|
||||||
|
Shoulda::Matchers.word_wrap(message)
|
||||||
end
|
end
|
||||||
|
|
||||||
def failure_message_when_negated
|
def failure_message_when_negated
|
||||||
"Expected #{expectation},\ngot#{error_description}"
|
validator = first_passing_validator
|
||||||
|
message = failure_message_preface.call + ' invalid'
|
||||||
|
|
||||||
|
if validator.type_of_message_matched?
|
||||||
|
if validator.has_messages?
|
||||||
|
message << ' and to'
|
||||||
|
|
||||||
|
if validator.captured_validation_exception?
|
||||||
|
message << ' raise a validation exception with message'
|
||||||
|
else
|
||||||
|
message << ' produce'
|
||||||
|
|
||||||
|
if expected_message.is_a?(Regexp)
|
||||||
|
message << ' a'
|
||||||
|
else
|
||||||
|
message << ' the'
|
||||||
|
end
|
||||||
|
|
||||||
|
message << ' validation error'
|
||||||
|
end
|
||||||
|
|
||||||
|
if expected_message.is_a?(Regexp)
|
||||||
|
message << ' matching'
|
||||||
|
end
|
||||||
|
|
||||||
|
message << " #{expected_message.inspect}"
|
||||||
|
|
||||||
|
unless validator.captured_validation_exception?
|
||||||
|
message << " on :#{attribute_to_check_message_against}"
|
||||||
|
end
|
||||||
|
|
||||||
|
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.'
|
||||||
|
else
|
||||||
|
message << " it produced these validation errors instead:\n\n"
|
||||||
|
message << validator.all_formatted_validation_error_messages
|
||||||
|
end
|
||||||
|
else
|
||||||
|
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 << ' 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
|
end
|
||||||
|
|
||||||
def description
|
def description
|
||||||
validator.allow_description(allowed_values)
|
ValidationMatcher::BuildDescription.call(self, simple_description)
|
||||||
|
end
|
||||||
|
|
||||||
|
def model
|
||||||
|
instance.class
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
attr_reader :instance, :attribute_to_check_message_against
|
attr_reader(
|
||||||
attr_accessor :values_to_match, :attribute_to_set, :value,
|
:after_setting_value_callback,
|
||||||
:matched_error, :after_setting_value_callback, :validator
|
:attribute_to_check_message_against,
|
||||||
|
:attribute_to_set,
|
||||||
|
:context,
|
||||||
|
:instance,
|
||||||
|
:options,
|
||||||
|
:values_to_set,
|
||||||
|
)
|
||||||
|
|
||||||
def instance=(instance)
|
private
|
||||||
@instance = instance
|
|
||||||
validator.record = instance
|
|
||||||
end
|
|
||||||
|
|
||||||
def attribute_to_check_message_against=(attribute)
|
|
||||||
@attribute_to_check_message_against = attribute
|
|
||||||
validator.attribute = attribute
|
|
||||||
end
|
|
||||||
|
|
||||||
def ignoring_interference_by_writer?
|
def ignoring_interference_by_writer?
|
||||||
@ignoring_interference_by_writer
|
@ignoring_interference_by_writer
|
||||||
end
|
end
|
||||||
|
|
||||||
def value_matches?(value)
|
def value_matches?(value, validator)
|
||||||
self.value = value
|
@last_value_set = value
|
||||||
set_attribute(value)
|
set_attribute(value)
|
||||||
!(errors_match? || any_range_error_occurred?)
|
!errors_match?(validator) && !any_range_error_occurred?(validator)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_attribute(value)
|
def set_attribute(value)
|
||||||
|
@ -468,64 +563,36 @@ https://github.com/thoughtbot/shoulda-matchers/issues
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def errors_match?
|
def errors_match?(validator)
|
||||||
has_messages? && errors_for_attribute_match?
|
validator.has_messages? &&
|
||||||
|
validator.type_of_message_matched? &&
|
||||||
|
errors_for_attribute_match?(validator)
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_messages?
|
def errors_for_attribute_match?(validator)
|
||||||
validator.has_messages?
|
matched_errors(validator).compact.any?
|
||||||
end
|
end
|
||||||
|
|
||||||
def errors_for_attribute_match?
|
def matched_errors(validator)
|
||||||
if expected_message
|
if expected_message
|
||||||
self.matched_error = errors_match_regexp? || errors_match_string?
|
validator.messages.grep(expected_message)
|
||||||
else
|
else
|
||||||
errors_for_attribute.compact.any?
|
validator.messages
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def errors_for_attribute
|
def any_range_error_occurred?(validator)
|
||||||
validator.formatted_messages
|
|
||||||
end
|
|
||||||
|
|
||||||
def errors_match_regexp?
|
|
||||||
if Regexp === expected_message
|
|
||||||
errors_for_attribute.detect { |e| e =~ expected_message }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def errors_match_string?
|
|
||||||
if errors_for_attribute.include?(expected_message)
|
|
||||||
expected_message
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def any_range_error_occurred?
|
|
||||||
validator.captured_range_error?
|
validator.captured_range_error?
|
||||||
end
|
end
|
||||||
|
|
||||||
def expectation
|
def inspected_values_to_set
|
||||||
parts = [
|
if values_to_set.size > 1
|
||||||
expected_messages_description,
|
values_to_set.map(&:inspect).to_sentence(
|
||||||
"when #{attribute_to_set} is set to #{value.inspect}"
|
two_words_connector: " or ",
|
||||||
]
|
last_word_connector: ", or"
|
||||||
|
)
|
||||||
parts.join(' ').squeeze(' ')
|
|
||||||
end
|
|
||||||
|
|
||||||
def expected_messages_description
|
|
||||||
validator.expected_messages_description(expected_message)
|
|
||||||
end
|
|
||||||
|
|
||||||
def error_description
|
|
||||||
validator.messages_description
|
|
||||||
end
|
|
||||||
|
|
||||||
def allowed_values
|
|
||||||
if values_to_match.length > 1
|
|
||||||
"any of [#{values_to_match.map(&:inspect).join(', ')}]"
|
|
||||||
else
|
else
|
||||||
values_to_match.first.inspect
|
values_to_set.first.inspect
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -540,7 +607,11 @@ https://github.com/thoughtbot/shoulda-matchers/issues
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_expected_message
|
def default_expected_message
|
||||||
validator.expected_message_from(default_attribute_message)
|
if expects_strict?
|
||||||
|
"#{human_attribute_name} #{default_attribute_message}"
|
||||||
|
else
|
||||||
|
default_attribute_message
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_attribute_message
|
def default_attribute_message
|
||||||
|
@ -560,9 +631,51 @@ https://github.com/thoughtbot/shoulda-matchers/issues
|
||||||
defaults.merge(options[:expected_message_values])
|
defaults.merge(options[:expected_message_values])
|
||||||
end
|
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
|
def model_name
|
||||||
instance.class.to_s.underscore
|
instance.class.to_s.underscore
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def human_attribute_name
|
||||||
|
instance.class.human_attribute_name(attribute_to_check_message_against)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,46 +7,55 @@ module Shoulda
|
||||||
class DisallowValueMatcher
|
class DisallowValueMatcher
|
||||||
extend Forwardable
|
extend Forwardable
|
||||||
|
|
||||||
def_delegators :allow_matcher, :_after_setting_value
|
def_delegators :allow_matcher,
|
||||||
|
:_after_setting_value,
|
||||||
|
:attribute_to_set,
|
||||||
|
:description,
|
||||||
|
:model,
|
||||||
|
:last_value_set,
|
||||||
|
:simple_description,
|
||||||
|
:failure_message_preface,
|
||||||
|
:failure_message_preface=
|
||||||
|
|
||||||
def initialize(value)
|
def initialize(value)
|
||||||
@allow_matcher = AllowValueMatcher.new(value)
|
@allow_matcher = AllowValueMatcher.new(value)
|
||||||
end
|
end
|
||||||
|
|
||||||
def matches?(subject)
|
def matches?(subject)
|
||||||
!@allow_matcher.matches?(subject)
|
!allow_matcher.matches?(subject)
|
||||||
end
|
end
|
||||||
|
|
||||||
def for(attribute)
|
def for(attribute)
|
||||||
@allow_matcher.for(attribute)
|
allow_matcher.for(attribute)
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
def on(context)
|
def on(context)
|
||||||
@allow_matcher.on(context)
|
allow_matcher.on(context)
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
def with_message(message, options={})
|
def with_message(message, options={})
|
||||||
@allow_matcher.with_message(message, options)
|
allow_matcher.with_message(message, options)
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def strict(strict = true)
|
||||||
|
allow_matcher.strict(strict)
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
def ignoring_interference_by_writer
|
def ignoring_interference_by_writer
|
||||||
@allow_matcher.ignoring_interference_by_writer
|
allow_matcher.ignoring_interference_by_writer
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
def failure_message
|
def failure_message
|
||||||
@allow_matcher.failure_message_when_negated
|
allow_matcher.failure_message_when_negated
|
||||||
end
|
end
|
||||||
|
|
||||||
def failure_message_when_negated
|
def failure_message_when_negated
|
||||||
@allow_matcher.failure_message
|
allow_matcher.failure_message
|
||||||
end
|
|
||||||
|
|
||||||
def strict
|
|
||||||
@allow_matcher.strict
|
|
||||||
self
|
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
|
@ -3,25 +3,17 @@ module Shoulda
|
||||||
module ActiveModel
|
module ActiveModel
|
||||||
# @private
|
# @private
|
||||||
module Helpers
|
module Helpers
|
||||||
def pretty_error_messages(obj)
|
def pretty_error_messages(object)
|
||||||
obj.errors.map do |attribute, message|
|
format_validation_errors(object.errors)
|
||||||
full_message = message.dup.inspect
|
end
|
||||||
parenthetical_parts = []
|
|
||||||
|
|
||||||
unless attribute.to_sym == :base
|
def format_validation_errors(errors)
|
||||||
parenthetical_parts << "attribute: #{attribute}"
|
list_items = errors.keys.map do |attribute|
|
||||||
|
messages = errors[attribute]
|
||||||
|
"* #{attribute}: #{messages}"
|
||||||
|
end
|
||||||
|
|
||||||
if obj.respond_to?(attribute)
|
list_items.join("\n")
|
||||||
parenthetical_parts << "value: #{obj.__send__(attribute).inspect}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if parenthetical_parts.any?
|
|
||||||
full_message << " (#{parenthetical_parts.join(', ')})"
|
|
||||||
end
|
|
||||||
|
|
||||||
"* " + full_message
|
|
||||||
end.join("\n")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_error_message(type, options = {})
|
def default_error_message(type, options = {})
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
module Shoulda
|
|
||||||
module Matchers
|
|
||||||
module ActiveModel
|
|
||||||
# @private
|
|
||||||
module StrictValidator
|
|
||||||
def allow_description(allowed_values)
|
|
||||||
"doesn't raise when #{attribute} is set to #{allowed_values}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def expected_message_from(attribute_message)
|
|
||||||
"#{human_attribute_name} #{attribute_message}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def formatted_messages
|
|
||||||
[messages.first.message]
|
|
||||||
end
|
|
||||||
|
|
||||||
def messages_description
|
|
||||||
if has_messages?
|
|
||||||
': ' + messages.first.message.inspect
|
|
||||||
else
|
|
||||||
' no exception'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def expected_messages_description(expected_message)
|
|
||||||
if expected_message
|
|
||||||
"exception to include #{expected_message.inspect}"
|
|
||||||
else
|
|
||||||
'an exception to have been raised'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
def collect_messages
|
|
||||||
validation_exceptions
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def validation_exceptions
|
|
||||||
record.valid?(context)
|
|
||||||
[]
|
|
||||||
rescue ::ActiveModel::StrictValidationFailed => exception
|
|
||||||
[exception]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -5,99 +5,104 @@ module Shoulda
|
||||||
class Validator
|
class Validator
|
||||||
include Helpers
|
include Helpers
|
||||||
|
|
||||||
attr_writer :attribute, :context, :record
|
attr_writer :context, :expects_strict
|
||||||
|
|
||||||
def initialize
|
def initialize(record, attribute)
|
||||||
|
@record = record
|
||||||
|
@attribute = attribute
|
||||||
|
@context = context
|
||||||
|
@expects_strict = false
|
||||||
reset
|
reset
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset
|
def reset
|
||||||
@messages = nil
|
@_validation_result = nil
|
||||||
end
|
@captured_validation_exception = false
|
||||||
|
@captured_range_error = false
|
||||||
def strict=(strict)
|
|
||||||
@strict = strict
|
|
||||||
|
|
||||||
if strict
|
|
||||||
extend StrictValidator
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def allow_description(allowed_values)
|
|
||||||
"allow #{attribute} to be set to #{allowed_values}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def expected_message_from(attribute_message)
|
|
||||||
attribute_message
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def messages
|
def messages
|
||||||
@messages ||= collect_messages
|
if expects_strict?
|
||||||
end
|
[validation_exception_message]
|
||||||
|
else
|
||||||
def formatted_messages
|
validation_error_messages
|
||||||
messages
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_messages?
|
def has_messages?
|
||||||
messages.any?
|
messages.any?
|
||||||
end
|
end
|
||||||
|
|
||||||
def messages_description
|
def type_of_message_matched?
|
||||||
if has_messages?
|
expects_strict? == captured_validation_exception?
|
||||||
" errors:\n#{pretty_error_messages(record)}"
|
|
||||||
else
|
|
||||||
' no errors'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def expected_messages_description(expected_message)
|
def captured_validation_exception?
|
||||||
if expected_message
|
@captured_validation_exception
|
||||||
"errors to include #{expected_message.inspect}"
|
|
||||||
else
|
|
||||||
'errors'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def captured_range_error?
|
def captured_range_error?
|
||||||
!!captured_range_error
|
!!@captured_range_error
|
||||||
|
end
|
||||||
|
|
||||||
|
def all_validation_errors
|
||||||
|
validation_result[:all_validation_errors]
|
||||||
|
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
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
attr_reader :attribute, :context, :strict, :record,
|
attr_reader :attribute, :context, :record
|
||||||
:captured_range_error
|
|
||||||
|
|
||||||
def collect_messages
|
|
||||||
validation_errors
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def strict?
|
def expects_strict?
|
||||||
!!@strict
|
@expects_strict
|
||||||
end
|
end
|
||||||
|
|
||||||
def collect_errors_or_exceptions
|
def validation_result
|
||||||
collect_messages
|
@_validation_result ||= perform_validation
|
||||||
end
|
end
|
||||||
|
|
||||||
def validation_errors
|
def perform_validation
|
||||||
if context
|
if context
|
||||||
record.valid?(context)
|
record.valid?(context)
|
||||||
else
|
else
|
||||||
record.valid?
|
record.valid?
|
||||||
end
|
end
|
||||||
|
|
||||||
if record.errors.respond_to?(:[])
|
all_validation_errors = record.errors.dup
|
||||||
record.errors[attribute]
|
|
||||||
else
|
|
||||||
record.errors.on(attribute)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def human_attribute_name
|
validation_error_messages =
|
||||||
record.class.human_attribute_name(attribute)
|
if record.errors.respond_to?(:[])
|
||||||
|
record.errors[attribute]
|
||||||
|
else
|
||||||
|
record.errors.on(attribute)
|
||||||
|
end
|
||||||
|
|
||||||
|
{
|
||||||
|
all_validation_errors: all_validation_errors,
|
||||||
|
validation_error_messages: validation_error_messages,
|
||||||
|
validation_exception_message: nil
|
||||||
|
}
|
||||||
|
rescue ::ActiveModel::StrictValidationFailed => exception
|
||||||
|
@captured_validation_exception = true
|
||||||
|
{
|
||||||
|
all_validation_errors: nil,
|
||||||
|
validation_error_messages: [],
|
||||||
|
validation_exception_message: exception.message
|
||||||
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,21 +13,24 @@ describe Shoulda::Matchers::ActiveModel::AllowValueMatcher, type: :model do
|
||||||
it 'describes itself with multiple values' do
|
it 'describes itself with multiple values' do
|
||||||
matcher = allow_value('foo', 'bar').for(:baz)
|
matcher = allow_value('foo', 'bar').for(:baz)
|
||||||
|
|
||||||
expect(matcher.description).to eq 'allow baz to be set to any of ["foo", "bar"]'
|
expect(matcher.description).to eq(
|
||||||
|
'allow :baz to be "foo" or "bar"'
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'describes itself with a single value' do
|
it 'describes itself with a single value' do
|
||||||
matcher = allow_value('foo').for(:baz)
|
matcher = allow_value('foo').for(:baz)
|
||||||
|
|
||||||
expect(matcher.description).to eq 'allow baz to be set to "foo"'
|
expect(matcher.description).to eq 'allow :baz to be "foo"'
|
||||||
end
|
end
|
||||||
|
|
||||||
if active_model_3_2?
|
if active_model_3_2?
|
||||||
it 'describes itself with a strict validation' do
|
it 'describes itself with a strict validation' do
|
||||||
strict_matcher = allow_value('xyz').for(:attr).strict
|
strict_matcher = allow_value('xyz').for(:attr).strict
|
||||||
|
|
||||||
expect(strict_matcher.description).
|
expect(strict_matcher.description).to eq(
|
||||||
to eq %q(doesn't raise when attr is set to "xyz")
|
'allow :attr to be "xyz", raising a validation exception on failure'
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -51,8 +54,19 @@ describe Shoulda::Matchers::ActiveModel::AllowValueMatcher, type: :model do
|
||||||
expect(validating_format(with: /abc/)).to allow_value('abcde').for(:attr)
|
expect(validating_format(with: /abc/)).to allow_value('abcde').for(:attr)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'rejects a bad value' do
|
it 'rejects a bad value with an appropriate failure message' do
|
||||||
expect(validating_format(with: /abc/)).not_to allow_value('xyz').for(:attr)
|
message = <<-MESSAGE
|
||||||
|
After setting :attr to "xyz", the matcher expected the Example to be
|
||||||
|
valid, but it was invalid instead, producing these validation errors:
|
||||||
|
|
||||||
|
* attr: ["is invalid"]
|
||||||
|
MESSAGE
|
||||||
|
|
||||||
|
assertion = lambda do
|
||||||
|
expect(validating_format(with: /abc/)).to allow_value('xyz').for(:attr)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(&assertion).to fail_with_message(message)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'allows several good values' do
|
it 'allows several good values' do
|
||||||
|
@ -60,11 +74,31 @@ describe Shoulda::Matchers::ActiveModel::AllowValueMatcher, type: :model do
|
||||||
to allow_value('abcde', 'deabc').for(:attr)
|
to allow_value('abcde', 'deabc').for(:attr)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'rejects several bad values' do
|
context 'given several bad values' do
|
||||||
expect(validating_format(with: /abc/)).
|
it 'rejects' do
|
||||||
not_to allow_value('xyz', 'zyx', nil, []).
|
expect(validating_format(with: /abc/)).
|
||||||
for(:attr).
|
not_to allow_value('xyz', 'zyx', nil, []).
|
||||||
ignoring_interference_by_writer
|
for(:attr).
|
||||||
|
ignoring_interference_by_writer
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'produces an appropriate failure message' do
|
||||||
|
message = <<-MESSAGE
|
||||||
|
After setting :attr to "zyx", the matcher expected the Example to be
|
||||||
|
valid, but it was invalid instead, producing these validation errors:
|
||||||
|
|
||||||
|
* attr: ["is invalid"]
|
||||||
|
MESSAGE
|
||||||
|
|
||||||
|
assertion = lambda do
|
||||||
|
expect(validating_format(with: /abc/)).
|
||||||
|
to allow_value('abc', 'abcde', 'zyx', nil, []).
|
||||||
|
for(:attr).
|
||||||
|
ignoring_interference_by_writer
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(&assertion).to fail_with_message(message)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -74,27 +108,95 @@ describe Shoulda::Matchers::ActiveModel::AllowValueMatcher, type: :model do
|
||||||
to allow_value('abcde').for(:attr).with_message(/bad/)
|
to allow_value('abcde').for(:attr).with_message(/bad/)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'rejects a bad value' do
|
it 'rejects a bad value with an appropriate failure message' do
|
||||||
expect(validating_format(with: /abc/, message: 'bad value')).
|
message = <<-MESSAGE
|
||||||
not_to allow_value('xyz').for(:attr).with_message(/bad/)
|
After setting :attr to "xyz", the matcher expected the Example to be
|
||||||
|
valid, but it was invalid instead, producing these validation errors:
|
||||||
|
|
||||||
|
* attr: ["bad value"]
|
||||||
|
MESSAGE
|
||||||
|
|
||||||
|
assertion = lambda do
|
||||||
|
expect(validating_format(with: /abc/, message: 'bad value')).
|
||||||
|
to allow_value('xyz').for(:attr).with_message(/bad/)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(&assertion).to fail_with_message(message)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'allows interpolation values for the message to be provided' do
|
context 'when the custom messages do not match' do
|
||||||
options = {
|
it 'rejects with an appropriate failure message' do
|
||||||
attribute_name: :attr,
|
message = <<-MESSAGE
|
||||||
attribute_type: :string
|
After setting :attr to "xyz", the matcher expected the Example to be
|
||||||
}
|
invalid and to produce a validation error matching /different/ on :attr.
|
||||||
|
The record was indeed invalid, but it produced these validation errors
|
||||||
|
instead:
|
||||||
|
|
||||||
record = record_with_custom_validation(options) do
|
* attr: ["bad value"]
|
||||||
if self.attr == 'xyz'
|
MESSAGE
|
||||||
self.errors.add :attr, :greater_than, count: 2
|
|
||||||
|
assertion = lambda do
|
||||||
|
expect(validating_format(with: /abc/, message: 'bad value')).
|
||||||
|
not_to allow_value('xyz').for(:attr).with_message(/different/)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(&assertion).to fail_with_message(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when interpolation values are provided along with a custom message' do
|
||||||
|
context 'when the messages match' do
|
||||||
|
it 'accepts' do
|
||||||
|
options = {
|
||||||
|
attribute_name: :attr,
|
||||||
|
attribute_type: :string
|
||||||
|
}
|
||||||
|
|
||||||
|
record = record_with_custom_validation(options) do
|
||||||
|
if self.attr == 'xyz'
|
||||||
|
self.errors.add :attr, :greater_than, count: 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(record).
|
||||||
|
not_to allow_value('xyz').
|
||||||
|
for(:attr).
|
||||||
|
with_message(:greater_than, values: { count: 2 })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(record).
|
context 'when the messages do not match' do
|
||||||
not_to allow_value('xyz').
|
it 'rejects with an appropriate failure message' do
|
||||||
for(:attr).
|
options = {
|
||||||
with_message(:greater_than, values: { count: 2 })
|
attribute_name: :attr,
|
||||||
|
attribute_type: :string
|
||||||
|
}
|
||||||
|
|
||||||
|
record = record_with_custom_validation(options) do
|
||||||
|
if self.attr == 'xyz'
|
||||||
|
self.errors.add :attr, "some other error"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assertion = lambda do
|
||||||
|
expect(record).
|
||||||
|
not_to allow_value('xyz').
|
||||||
|
for(:attr).
|
||||||
|
with_message(:greater_than, values: { count: 2 })
|
||||||
|
end
|
||||||
|
|
||||||
|
message = <<-MESSAGE
|
||||||
|
After setting :attr to "xyz", the matcher expected the Example to be
|
||||||
|
invalid and to produce the validation error "must be greater than 2" on
|
||||||
|
:attr. The record was indeed invalid, but it produced these validation
|
||||||
|
errors instead:
|
||||||
|
|
||||||
|
* attr: ["some other error"]
|
||||||
|
MESSAGE
|
||||||
|
|
||||||
|
expect(&assertion).to fail_with_message(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -102,25 +204,62 @@ describe Shoulda::Matchers::ActiveModel::AllowValueMatcher, type: :model do
|
||||||
include UnitTests::AllowValueMatcherHelpers
|
include UnitTests::AllowValueMatcherHelpers
|
||||||
|
|
||||||
context 'when the validation error message was provided directly' do
|
context 'when the validation error message was provided directly' do
|
||||||
it 'passes given a valid value' do
|
context 'given a valid value' do
|
||||||
builder = builder_for_record_with_different_error_attribute
|
it 'accepts' do
|
||||||
expect(builder.record).
|
builder = builder_for_record_with_different_error_attribute
|
||||||
to allow_value(builder.valid_value).
|
expect(builder.record).
|
||||||
for(builder.attribute_to_validate).
|
to allow_value(builder.valid_value).
|
||||||
with_message(builder.message,
|
for(builder.attribute_to_validate).
|
||||||
against: builder.attribute_that_receives_error
|
with_message(
|
||||||
)
|
builder.message,
|
||||||
|
against: builder.attribute_that_receives_error
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'fails given an invalid value' do
|
context 'given an invalid value' do
|
||||||
builder = builder_for_record_with_different_error_attribute
|
it 'rejects' do
|
||||||
invalid_value = "#{builder.valid_value} (invalid)"
|
builder = builder_for_record_with_different_error_attribute
|
||||||
expect(builder.record).
|
invalid_value = "#{builder.valid_value} (invalid)"
|
||||||
not_to allow_value(invalid_value).
|
|
||||||
for(builder.attribute_to_validate).
|
expect(builder.record).
|
||||||
with_message(builder.message,
|
not_to allow_value(invalid_value).
|
||||||
against: builder.attribute_that_receives_error
|
for(builder.attribute_to_validate).
|
||||||
)
|
with_message(
|
||||||
|
builder.message,
|
||||||
|
against: builder.attribute_that_receives_error
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'if the messages do not match' do
|
||||||
|
it 'technically accepts' do
|
||||||
|
builder = builder_for_record_with_different_error_attribute(
|
||||||
|
message: "a different error"
|
||||||
|
)
|
||||||
|
invalid_value = "#{builder.valid_value} (invalid)"
|
||||||
|
|
||||||
|
assertion = lambda do
|
||||||
|
expect(builder.record).
|
||||||
|
not_to allow_value(invalid_value).
|
||||||
|
for(builder.attribute_to_validate).
|
||||||
|
with_message(
|
||||||
|
"some error",
|
||||||
|
against: builder.attribute_that_receives_error
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
message = <<-MESSAGE
|
||||||
|
After setting :#{builder.attribute_to_validate} to "#{invalid_value}", the
|
||||||
|
matcher expected the #{builder.model.name} to be invalid and to produce the validation
|
||||||
|
error "some error" on :#{builder.attribute_that_receives_error}. The record was
|
||||||
|
indeed invalid, but it produced these validation errors instead:
|
||||||
|
|
||||||
|
* #{builder.attribute_that_receives_error}: ["a different error"]
|
||||||
|
MESSAGE
|
||||||
|
|
||||||
|
expect(&assertion).to fail_with_message(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -130,7 +269,8 @@ describe Shoulda::Matchers::ActiveModel::AllowValueMatcher, type: :model do
|
||||||
expect(builder.record).
|
expect(builder.record).
|
||||||
to allow_value(builder.valid_value).
|
to allow_value(builder.valid_value).
|
||||||
for(builder.attribute_to_validate).
|
for(builder.attribute_to_validate).
|
||||||
with_message(builder.validation_message_key,
|
with_message(
|
||||||
|
builder.validation_message_key,
|
||||||
against: builder.attribute_that_receives_error
|
against: builder.attribute_that_receives_error
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -141,7 +281,8 @@ describe Shoulda::Matchers::ActiveModel::AllowValueMatcher, type: :model do
|
||||||
expect(builder.record).
|
expect(builder.record).
|
||||||
not_to allow_value(invalid_value).
|
not_to allow_value(invalid_value).
|
||||||
for(builder.attribute_to_validate).
|
for(builder.attribute_to_validate).
|
||||||
with_message(builder.validation_message_key,
|
with_message(
|
||||||
|
builder.validation_message_key,
|
||||||
against: builder.attribute_that_receives_error
|
against: builder.attribute_that_receives_error
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -199,11 +340,16 @@ describe Shoulda::Matchers::ActiveModel::AllowValueMatcher, type: :model do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "does not match given good values along with bad values" do
|
it "does not match given good values along with bad values" do
|
||||||
message = %{Expected errors when attr is set to "12345",\ngot no errors}
|
message = <<-MESSAGE.strip_heredoc
|
||||||
|
After setting :attr to "12345", the matcher expected the Example to be
|
||||||
|
invalid, but it was valid instead.
|
||||||
|
MESSAGE
|
||||||
|
|
||||||
expect {
|
assertion = lambda do
|
||||||
expect(model).not_to allow_value('12345', *bad_values).for(:attr)
|
expect(model).not_to allow_value('12345', *bad_values).for(:attr)
|
||||||
}.to fail_with_message(message)
|
end
|
||||||
|
|
||||||
|
expect(&assertion).to fail_with_message(message)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -226,25 +372,39 @@ describe Shoulda::Matchers::ActiveModel::AllowValueMatcher, type: :model do
|
||||||
|
|
||||||
if active_model_3_2?
|
if active_model_3_2?
|
||||||
context 'an attribute with a strict format validation' do
|
context 'an attribute with a strict format validation' do
|
||||||
it 'strictly rejects a bad value' do
|
context 'when qualified with strict' do
|
||||||
expect(validating_format(with: /abc/, strict: true)).
|
it 'rejects a bad value, providing the correct failure message' do
|
||||||
not_to allow_value('xyz').for(:attr).strict
|
message = <<-MESSAGE.strip_heredoc
|
||||||
end
|
After setting :attr to "xyz", the matcher expected the Example to be
|
||||||
|
valid, but it was invalid instead, raising a validation exception with
|
||||||
|
the message "Attr is invalid".
|
||||||
|
MESSAGE
|
||||||
|
|
||||||
it 'strictly allows a bad value with a different message' do
|
assertion = lambda do
|
||||||
expect(validating_format(with: /abc/, strict: true)).
|
expect(validating_format(with: /abc/, strict: true)).
|
||||||
to allow_value('xyz').for(:attr).with_message(/abc/).strict
|
to allow_value('xyz').for(:attr).strict
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'provides a useful negative failure message' do
|
expect(&assertion).to fail_with_message(message)
|
||||||
matcher = allow_value('xyz').for(:attr).strict.with_message(/abc/)
|
end
|
||||||
|
|
||||||
matcher.matches?(validating_format(with: /abc/, strict: true))
|
context 'qualified with a custom message' do
|
||||||
|
it 'rejects a bad value when the failure messages do not match' do
|
||||||
|
message = <<-MESSAGE.strip_heredoc
|
||||||
|
After setting :attr to "xyz", the matcher expected the Example to be
|
||||||
|
invalid and to raise a validation exception with message matching /abc/.
|
||||||
|
The record was indeed invalid, but the exception message was "Attr is
|
||||||
|
invalid" instead.
|
||||||
|
MESSAGE
|
||||||
|
|
||||||
expect(matcher.failure_message_when_negated).to eq(
|
assertion = lambda do
|
||||||
%{Expected exception to include /abc/ when attr is set to "xyz",\n} +
|
expect(validating_format(with: /abc/, strict: true)).
|
||||||
%{got: "Attr is invalid"}
|
not_to allow_value('xyz').for(:attr).with_message(/abc/).strict
|
||||||
)
|
end
|
||||||
|
|
||||||
|
expect(&assertion).to fail_with_message(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue