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:
Elliot Winkler 2015-12-19 10:11:01 -07:00
parent 6a4871547a
commit 2962112114
15 changed files with 756 additions and 244 deletions

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,14 @@
module Shoulda
module Matchers
module ActiveModel
class AllowValueMatcher
# @private
class SuccessfulCheck
def successful?
true
end
end
end
end
end
end

View File

@ -0,0 +1,14 @@
module Shoulda
module Matchers
module ActiveModel
class AllowValueMatcher
# @private
class SuccessfulSetting
def successful?
true
end
end
end
end
end
end

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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