2014-01-23 18:07:36 +00:00
|
|
|
module Shoulda
|
2010-12-15 22:34:19 +00:00
|
|
|
module Matchers
|
2014-01-23 18:07:36 +00:00
|
|
|
module ActiveModel
|
2015-09-30 18:51:01 +00:00
|
|
|
# The `allow_value` matcher (or its alias, `allow_values`) is used to
|
|
|
|
# ensure that an attribute is valid or invalid if set to one or more
|
|
|
|
# values.
|
2014-01-23 18:07:36 +00:00
|
|
|
#
|
2015-09-30 18:51:01 +00:00
|
|
|
# Take this model for example:
|
2014-01-23 18:07:36 +00:00
|
|
|
#
|
|
|
|
# class UserProfile
|
|
|
|
# include ActiveModel::Model
|
|
|
|
# attr_accessor :website_url
|
|
|
|
#
|
|
|
|
# validates_format_of :website_url, with: URI.regexp
|
|
|
|
# end
|
|
|
|
#
|
2015-09-30 18:51:01 +00:00
|
|
|
# You can use `allow_value` to test one value at a time:
|
|
|
|
#
|
|
|
|
# # RSpec
|
|
|
|
# describe UserProfile do
|
|
|
|
# it { should allow_value('http://foo.com').for(:website_url) }
|
|
|
|
# it { should allow_value('http://bar.com').for(:website_url) }
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # Test::Unit
|
|
|
|
# class UserProfileTest < ActiveSupport::TestCase
|
|
|
|
# should allow_value('http://foo.com').for(:website_url)
|
|
|
|
# should allow_value('http://bar.com').for(:website_url)
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# You can also test multiple values in one go, if you like. In the
|
|
|
|
# positive sense, this makes an assertion that none of the values cause the
|
|
|
|
# record to be invalid. In the negative sense, this makes an assertion
|
|
|
|
# that none of the values cause the record to be valid:
|
|
|
|
#
|
2014-01-23 18:07:36 +00:00
|
|
|
# # RSpec
|
|
|
|
# describe UserProfile do
|
|
|
|
# it do
|
2015-09-30 18:51:01 +00:00
|
|
|
# should allow_values('http://foo.com', 'http://bar.com').
|
|
|
|
# for(:website_url)
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# it do
|
|
|
|
# should_not allow_values('http://foo.com', 'buz').
|
2014-01-23 18:07:36 +00:00
|
|
|
# for(:website_url)
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # Test::Unit
|
|
|
|
# class UserProfileTest < ActiveSupport::TestCase
|
2015-09-30 18:51:01 +00:00
|
|
|
# should allow_values('http://foo.com', 'http://bar.com/baz').
|
|
|
|
# for(:website_url)
|
|
|
|
#
|
|
|
|
# should_not allow_values('http://foo.com', 'buz').
|
2014-01-23 18:07:36 +00:00
|
|
|
# for(:website_url)
|
|
|
|
# end
|
|
|
|
#
|
2015-09-30 18:51:01 +00:00
|
|
|
# #### Caveats
|
2014-01-23 18:07:36 +00:00
|
|
|
#
|
2015-09-30 18:51:01 +00:00
|
|
|
# When using `allow_value` or any matchers that depend on it, you may
|
|
|
|
# encounter a CouldNotSetAttributeError. This exception is raised if the
|
|
|
|
# matcher, in attempting to set a value on the attribute, detects that
|
|
|
|
# the value set is different from the value that the attribute returns
|
|
|
|
# upon reading it back.
|
2014-01-23 18:07:36 +00:00
|
|
|
#
|
2015-09-30 18:51:01 +00:00
|
|
|
# This usually happens if the writer method (`foo=`, `bar=`, etc.) for
|
|
|
|
# that attribute has custom logic to ignore certain incoming values or
|
|
|
|
# change them in any way. Here are three examples we've seen:
|
2014-01-23 18:07:36 +00:00
|
|
|
#
|
2015-09-30 18:51:01 +00:00
|
|
|
# * You're attempting to assert that an attribute should not allow nil,
|
|
|
|
# yet the attribute's writer method contains a conditional to do nothing
|
|
|
|
# if the attribute is set to nil:
|
2014-01-23 18:07:36 +00:00
|
|
|
#
|
2015-09-30 18:51:01 +00:00
|
|
|
# class Foo
|
|
|
|
# include ActiveModel::Model
|
2014-01-23 18:07:36 +00:00
|
|
|
#
|
2015-09-30 18:51:01 +00:00
|
|
|
# attr_reader :bar
|
2014-01-23 18:07:36 +00:00
|
|
|
#
|
2015-09-30 18:51:01 +00:00
|
|
|
# def bar=(value)
|
|
|
|
# return if value.nil?
|
|
|
|
# @bar = value
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# describe Foo do
|
|
|
|
# it do
|
|
|
|
# foo = Foo.new
|
|
|
|
# foo.bar = "baz"
|
|
|
|
# # This will raise a CouldNotSetAttributeError since `foo.bar` is now "123"
|
|
|
|
# expect(foo).not_to allow_value(nil).for(:bar)
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# * You're attempting to assert that an numeric attribute should not allow a
|
|
|
|
# string that contains non-numeric characters, yet the writer method for
|
|
|
|
# that attribute strips out non-numeric characters:
|
|
|
|
#
|
|
|
|
# class Foo
|
|
|
|
# include ActiveModel::Model
|
|
|
|
#
|
|
|
|
# attr_reader :bar
|
|
|
|
#
|
|
|
|
# def bar=(value)
|
|
|
|
# @bar = value.gsub(/\D+/, '')
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# describe Foo do
|
|
|
|
# it do
|
|
|
|
# foo = Foo.new
|
|
|
|
# # This will raise a CouldNotSetAttributeError since `foo.bar` is now "123"
|
|
|
|
# expect(foo).not_to allow_value("abc123").for(:bar)
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# * You're passing a value to `allow_value` that the model typecasts into
|
|
|
|
# another value:
|
|
|
|
#
|
|
|
|
# describe Foo do
|
|
|
|
# # Assume that `attr` is a string
|
|
|
|
# # This will raise a CouldNotSetAttributeError since `attr` typecasts `[]` to `"[]"`
|
|
|
|
# it { should_not allow_value([]).for(:attr) }
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# So when you encounter this exception, you have a couple of options:
|
|
|
|
#
|
|
|
|
# * If you understand the problem and wish to override this behavior to
|
|
|
|
# get around this exception, you can add the
|
|
|
|
# `ignoring_interference_by_writer` qualifier like so:
|
|
|
|
#
|
|
|
|
# it do
|
|
|
|
# should_not allow_value([]).
|
|
|
|
# for(:attr).
|
|
|
|
# ignoring_interference_by_writer
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# * Note, however, that the above option will not always cause the test to
|
|
|
|
# pass. In this case, this is telling you that you don't need to use
|
|
|
|
# `allow_value`, or quite possibly even the validation that you're
|
|
|
|
# testing altogether. In any case, we would probably make the argument
|
|
|
|
# that since it's clear that something is responsible for sanitizing
|
|
|
|
# incoming data before it's stored in your model, there's no need to
|
|
|
|
# ensure that sanitization places the model in a valid state, if such
|
|
|
|
# sanitization creates valid data. In terms of testing, the sanitization
|
|
|
|
# code should probably be tested, but not the effects of that
|
|
|
|
# sanitization on the validness of the model.
|
2014-01-23 18:07:36 +00:00
|
|
|
#
|
|
|
|
# #### Qualifiers
|
|
|
|
#
|
|
|
|
# ##### on
|
|
|
|
#
|
|
|
|
# Use `on` if your validation applies only under a certain context.
|
|
|
|
#
|
|
|
|
# class UserProfile
|
|
|
|
# include ActiveModel::Model
|
|
|
|
# attr_accessor :birthday_as_string
|
|
|
|
#
|
|
|
|
# validates_format_of :birthday_as_string,
|
|
|
|
# with: /^(\d+)-(\d+)-(\d+)$/,
|
|
|
|
# on: :create
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # RSpec
|
|
|
|
# describe UserProfile do
|
|
|
|
# it do
|
|
|
|
# should allow_value('2013-01-01').
|
|
|
|
# for(:birthday_as_string).
|
|
|
|
# on(:create)
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # Test::Unit
|
|
|
|
# class UserProfileTest < ActiveSupport::TestCase
|
|
|
|
# should allow_value('2013-01-01').
|
|
|
|
# for(:birthday_as_string).
|
|
|
|
# on(:create)
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# ##### with_message
|
|
|
|
#
|
|
|
|
# Use `with_message` if you are using a custom validation message.
|
|
|
|
#
|
|
|
|
# class UserProfile
|
|
|
|
# include ActiveModel::Model
|
|
|
|
# attr_accessor :state
|
|
|
|
#
|
|
|
|
# validates_format_of :state,
|
|
|
|
# with: /^(open|closed)$/,
|
|
|
|
# message: 'State must be open or closed'
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # RSpec
|
|
|
|
# describe UserProfile do
|
|
|
|
# it do
|
|
|
|
# should allow_value('open', 'closed').
|
|
|
|
# for(:state).
|
|
|
|
# with_message('State must be open or closed')
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # Test::Unit
|
|
|
|
# class UserProfileTest < ActiveSupport::TestCase
|
|
|
|
# should allow_value('open', 'closed').
|
|
|
|
# for(:state).
|
|
|
|
# with_message('State must be open or closed')
|
|
|
|
# end
|
|
|
|
#
|
2015-03-01 17:48:07 +00:00
|
|
|
# Use `with_message` with a regexp to perform a partial match:
|
|
|
|
#
|
|
|
|
# class UserProfile
|
|
|
|
# include ActiveModel::Model
|
|
|
|
# attr_accessor :state
|
|
|
|
#
|
|
|
|
# validates_format_of :state,
|
|
|
|
# with: /^(open|closed)$/,
|
|
|
|
# message: 'State must be open or closed'
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # RSpec
|
|
|
|
# describe UserProfile do
|
|
|
|
# it do
|
|
|
|
# should allow_value('open', 'closed').
|
|
|
|
# for(:state).
|
|
|
|
# with_message(/open or closed/)
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # Test::Unit
|
|
|
|
# class UserProfileTest < ActiveSupport::TestCase
|
|
|
|
# should allow_value('open', 'closed').
|
|
|
|
# for(:state).
|
|
|
|
# with_message(/open or closed/)
|
|
|
|
# end
|
|
|
|
#
|
2014-01-23 18:07:36 +00:00
|
|
|
# Use `with_message` with the `:against` option if the attribute the
|
|
|
|
# validation message is stored under is different from the attribute
|
2015-03-01 17:48:07 +00:00
|
|
|
# being validated:
|
2014-01-23 18:07:36 +00:00
|
|
|
#
|
|
|
|
# class UserProfile
|
|
|
|
# include ActiveModel::Model
|
|
|
|
# attr_accessor :sports_team
|
|
|
|
#
|
|
|
|
# validate :sports_team_must_be_valid
|
|
|
|
#
|
|
|
|
# private
|
|
|
|
#
|
|
|
|
# def sports_team_must_be_valid
|
|
|
|
# if sports_team !~ /^(Broncos|Titans)$/i
|
|
|
|
# self.errors.add :chosen_sports_team,
|
|
|
|
# 'Must be either a Broncos fan or a Titans fan'
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # RSpec
|
|
|
|
# describe UserProfile do
|
|
|
|
# it do
|
|
|
|
# should allow_value('Broncos', 'Titans').
|
|
|
|
# for(:sports_team).
|
|
|
|
# with_message('Must be either a Broncos or Titans fan',
|
|
|
|
# against: :chosen_sports_team
|
|
|
|
# )
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # Test::Unit
|
|
|
|
# class UserProfileTest < ActiveSupport::TestCase
|
|
|
|
# should allow_value('Broncos', 'Titans').
|
|
|
|
# for(:sports_team).
|
|
|
|
# with_message('Must be either a Broncos or Titans fan',
|
|
|
|
# against: :chosen_sports_team
|
|
|
|
# )
|
|
|
|
# end
|
|
|
|
#
|
2015-09-30 18:51:01 +00:00
|
|
|
# ##### ignoring_interference_by_writer
|
|
|
|
#
|
|
|
|
# Use `ignoring_interference_by_writer` if you've encountered a
|
|
|
|
# CouldNotSetAttributeError and wish to ignore it. Please read the Caveats
|
|
|
|
# section above for more information.
|
|
|
|
#
|
|
|
|
# class Address < ActiveRecord::Base
|
|
|
|
# # Address has a zip_code field which is a string
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # RSpec
|
|
|
|
# describe Address do
|
|
|
|
# it do
|
|
|
|
# should_not allow_value([]).
|
|
|
|
# for(:zip_code).
|
|
|
|
# ignoring_interference_by_writer
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # Test::Unit
|
|
|
|
# class AddressTest < ActiveSupport::TestCase
|
|
|
|
# should_not allow_value([]).
|
|
|
|
# for(:zip_code).
|
|
|
|
# ignoring_interference_by_writer
|
|
|
|
# end
|
|
|
|
#
|
2014-01-23 18:07:36 +00:00
|
|
|
# @return [AllowValueMatcher]
|
2010-12-15 22:34:19 +00:00
|
|
|
#
|
2010-09-05 15:33:32 +00:00
|
|
|
def allow_value(*values)
|
2012-03-23 14:10:08 +00:00
|
|
|
if values.empty?
|
2012-12-20 05:04:27 +00:00
|
|
|
raise ArgumentError, 'need at least one argument'
|
2012-03-23 14:10:08 +00:00
|
|
|
else
|
|
|
|
AllowValueMatcher.new(*values)
|
|
|
|
end
|
2010-12-15 22:34:19 +00:00
|
|
|
end
|
2015-09-30 18:51:01 +00:00
|
|
|
# @private
|
2015-03-30 21:43:16 +00:00
|
|
|
alias_method :allow_values, :allow_value
|
2010-12-15 22:34:19 +00:00
|
|
|
|
2014-01-23 18:07:36 +00:00
|
|
|
# @private
|
|
|
|
class AllowValueMatcher
|
allow_value: Raise error if attr sets value differently
`allow_value` will now raise a CouldNotSetAttribute error if the
attribute in question cannot be changed from a non-nil value to a nil
value, or vice versa. In other words, these are the exact cases in which
the error will occur:
* If you're testing whether the attribute allows `nil`, but the
attribute detects and ignores nil. (For instance, you have a model
that `has_secure_password`. This will add a #password= method to your
model that is defined in a such a way that you cannot clear the
password by setting it to nil -- nothing happens.)
* If you're testing whether the attribute allows a non-nil value, but
the attribute fails to set that value. (For instance, you have an
ActiveRecord model. If ActiveRecord cannot typecast the value in the
context of the column, then it will do nothing, and the attribute will be
effectively set to nil.)
What's the reasoning behind this change? Simply put, if you are assuming
that the attribute is changing but in fact it is not, then the test
you're writing isn't the test that actually gets run. We feel that this
is dishonest and produces an invalid test.
2013-11-22 20:46:59 +00:00
|
|
|
# @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
|
|
|
|
"Expected #{model.class} to be able to set #{attribute} to #{expected_value.inspect}, but got #{actual_value.inspect} instead."
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2010-12-15 22:34:19 +00:00
|
|
|
include Helpers
|
|
|
|
|
2013-07-24 21:46:01 +00:00
|
|
|
attr_accessor :attribute_with_message
|
2013-03-28 18:48:45 +00:00
|
|
|
attr_accessor :options
|
|
|
|
|
2010-09-05 15:33:32 +00:00
|
|
|
def initialize(*values)
|
2013-03-28 18:48:45 +00:00
|
|
|
self.values_to_match = values
|
|
|
|
self.options = {}
|
2014-04-16 06:24:02 +00:00
|
|
|
self.after_setting_value_callback = -> {}
|
2015-01-21 22:46:26 +00:00
|
|
|
self.validator = Validator.new
|
Tighten CouldNotSetAttributeError restriction
Why:
* Previously, `allow_value` would raise a CouldNotSetAttributeError
if the value being set didn't match the value the attribute had after
being set, but only if the attribute was being changed from nil to
non-nil or non-nil to nil.
* It turns out it doesn't matter which value you're trying to set the
attribute to -- if the attribute rejects that change it's confusing
either way. (In fact, I was recently bit by a case in which I was
trying to validate numericality of an attribute, where the writer
method for that attribute was overridden to ensure that the attribute
always stored a number and never contained non-number characters.
This ended up making the numericality validation useless, of
course -- but it caused confusion because the test acted in a way
I didn't expect.)
To satisfy the above:
* `allow_value` now raises a CouldNotSetAttributeError if the attribute
rejects the value being set in *any* way.
* However, add a `ignoring_interference_by_writer` qualifier so that
it is possible to manually override this behavior.
* Fix tests that are failing now because of this new change:
* Fix tests for allow_value matcher
* Fix tests for numericality matcher
* Remove tests for numericality matcher + integer column
* An integer column will typecast any non-integer value to an
integer.
* Because of the typecasting, our tests for the numericality matcher
against an integer column don't quite work, because we can't
really test what happens when the attribute is set to a
non-integer value. Now that `allow_value` is more strict, we're
getting a CouldNotSetAttributeError when attempting to do so.
* The tests mentioned were originally added to ensure that we are
handling RangeErrors that ActiveRecord used to emit. This doesn't
happen anymore, so the tests aren't necessary anymore either.
* Fix tests for acceptance matcher
* Fix tests for absence matcher
2015-09-26 05:10:00 +00:00
|
|
|
@ignoring_interference_by_writer = false
|
2010-12-15 22:34:19 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def for(attribute)
|
2013-07-24 21:46:01 +00:00
|
|
|
self.attribute_to_set = attribute
|
|
|
|
self.attribute_to_check_message_against = attribute
|
2010-12-15 22:34:19 +00:00
|
|
|
self
|
|
|
|
end
|
|
|
|
|
2013-03-04 03:34:38 +00:00
|
|
|
def on(context)
|
2015-01-21 22:46:26 +00:00
|
|
|
validator.context = context
|
2013-03-04 03:34:38 +00:00
|
|
|
self
|
|
|
|
end
|
|
|
|
|
2013-07-24 21:46:01 +00:00
|
|
|
def with_message(message, options={})
|
2013-03-28 18:48:45 +00:00
|
|
|
self.options[:expected_message] = message
|
2014-04-26 15:10:17 +00:00
|
|
|
self.options[:expected_message_values] = options.fetch(:values, {})
|
|
|
|
|
2013-07-24 21:46:01 +00:00
|
|
|
if options.key?(:against)
|
|
|
|
self.attribute_to_check_message_against = options[:against]
|
|
|
|
end
|
2014-04-26 15:10:17 +00:00
|
|
|
|
2010-12-15 22:34:19 +00:00
|
|
|
self
|
|
|
|
end
|
|
|
|
|
2012-09-11 22:40:39 +00:00
|
|
|
def strict
|
2015-01-21 22:46:26 +00:00
|
|
|
validator.strict = true
|
2012-09-11 22:40:39 +00:00
|
|
|
self
|
|
|
|
end
|
|
|
|
|
Tighten CouldNotSetAttributeError restriction
Why:
* Previously, `allow_value` would raise a CouldNotSetAttributeError
if the value being set didn't match the value the attribute had after
being set, but only if the attribute was being changed from nil to
non-nil or non-nil to nil.
* It turns out it doesn't matter which value you're trying to set the
attribute to -- if the attribute rejects that change it's confusing
either way. (In fact, I was recently bit by a case in which I was
trying to validate numericality of an attribute, where the writer
method for that attribute was overridden to ensure that the attribute
always stored a number and never contained non-number characters.
This ended up making the numericality validation useless, of
course -- but it caused confusion because the test acted in a way
I didn't expect.)
To satisfy the above:
* `allow_value` now raises a CouldNotSetAttributeError if the attribute
rejects the value being set in *any* way.
* However, add a `ignoring_interference_by_writer` qualifier so that
it is possible to manually override this behavior.
* Fix tests that are failing now because of this new change:
* Fix tests for allow_value matcher
* Fix tests for numericality matcher
* Remove tests for numericality matcher + integer column
* An integer column will typecast any non-integer value to an
integer.
* Because of the typecasting, our tests for the numericality matcher
against an integer column don't quite work, because we can't
really test what happens when the attribute is set to a
non-integer value. Now that `allow_value` is more strict, we're
getting a CouldNotSetAttributeError when attempting to do so.
* The tests mentioned were originally added to ensure that we are
handling RangeErrors that ActiveRecord used to emit. This doesn't
happen anymore, so the tests aren't necessary anymore either.
* Fix tests for acceptance matcher
* Fix tests for absence matcher
2015-09-26 05:10:00 +00:00
|
|
|
def ignoring_interference_by_writer
|
|
|
|
@ignoring_interference_by_writer = true
|
|
|
|
self
|
|
|
|
end
|
|
|
|
|
2014-07-24 04:03:51 +00:00
|
|
|
def _after_setting_value(&callback)
|
2014-04-16 06:24:02 +00:00
|
|
|
self.after_setting_value_callback = callback
|
|
|
|
end
|
|
|
|
|
2010-12-15 22:34:19 +00:00
|
|
|
def matches?(instance)
|
2013-03-28 18:48:45 +00:00
|
|
|
self.instance = instance
|
2014-10-22 07:18:54 +00:00
|
|
|
values_to_match.all? { |value| value_matches?(value) }
|
|
|
|
end
|
2013-03-26 22:16:04 +00:00
|
|
|
|
2014-10-22 07:18:54 +00:00
|
|
|
def does_not_match?(instance)
|
|
|
|
self.instance = instance
|
|
|
|
values_to_match.all? { |value| !value_matches?(value) }
|
2010-12-15 22:34:19 +00:00
|
|
|
end
|
|
|
|
|
2013-12-24 11:24:27 +00:00
|
|
|
def failure_message
|
2015-01-21 22:46:26 +00:00
|
|
|
"Did not expect #{expectation},\ngot#{error_description}"
|
2010-12-15 22:34:19 +00:00
|
|
|
end
|
|
|
|
|
2013-12-24 11:24:27 +00:00
|
|
|
def failure_message_when_negated
|
Tweak allow_value failure_message
This commit changes the failure message that `allow_value` generates so
that it reads a bit nicer.
When a call to #valid? resulted in validation messages, `allow_value`
formerly failed with this message:
Expected errors to include "the message" when attr is set to "some value", got errors: ["another message (attribute: \"attr\", value: \"some value\")", "some other message (attribute: \"attr2\", value: \"some other value\")"]
Now it fails with this message:
Expected errors to include "the message" when attr is set to "some value",
got errors:
* "another message" (attribute: attr, value: "some value")
* "some other message" (attribute: attr2, value: "some other value")
Similarly, when a call to #valid resulted in an exception, `allow_value`
formerly failed with this message:
Expected errors to include "the message" when attr is set to "some value", got: some message
Now it fails with this message:
Expected errors to include "the message" when attr is set to "some value",
got: "some message"
2014-10-12 02:19:23 +00:00
|
|
|
"Expected #{expectation},\ngot#{error_description}"
|
2010-12-15 22:34:19 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def description
|
2015-01-21 22:46:26 +00:00
|
|
|
validator.allow_description(allowed_values)
|
2010-12-15 22:34:19 +00:00
|
|
|
end
|
|
|
|
|
2014-06-24 15:57:52 +00:00
|
|
|
protected
|
2010-12-15 22:34:19 +00:00
|
|
|
|
2014-10-22 07:18:54 +00:00
|
|
|
attr_reader :instance, :attribute_to_check_message_against
|
|
|
|
attr_accessor :values_to_match, :attribute_to_set, :value,
|
2015-01-21 22:46:26 +00:00
|
|
|
:matched_error, :after_setting_value_callback, :validator
|
2013-11-22 20:46:59 +00:00
|
|
|
|
2014-10-22 07:18:54 +00:00
|
|
|
def instance=(instance)
|
|
|
|
@instance = instance
|
|
|
|
validator.record = instance
|
|
|
|
end
|
|
|
|
|
2015-01-21 22:46:26 +00:00
|
|
|
def attribute_to_check_message_against=(attribute)
|
|
|
|
@attribute_to_check_message_against = attribute
|
|
|
|
validator.attribute = attribute
|
|
|
|
end
|
|
|
|
|
Tighten CouldNotSetAttributeError restriction
Why:
* Previously, `allow_value` would raise a CouldNotSetAttributeError
if the value being set didn't match the value the attribute had after
being set, but only if the attribute was being changed from nil to
non-nil or non-nil to nil.
* It turns out it doesn't matter which value you're trying to set the
attribute to -- if the attribute rejects that change it's confusing
either way. (In fact, I was recently bit by a case in which I was
trying to validate numericality of an attribute, where the writer
method for that attribute was overridden to ensure that the attribute
always stored a number and never contained non-number characters.
This ended up making the numericality validation useless, of
course -- but it caused confusion because the test acted in a way
I didn't expect.)
To satisfy the above:
* `allow_value` now raises a CouldNotSetAttributeError if the attribute
rejects the value being set in *any* way.
* However, add a `ignoring_interference_by_writer` qualifier so that
it is possible to manually override this behavior.
* Fix tests that are failing now because of this new change:
* Fix tests for allow_value matcher
* Fix tests for numericality matcher
* Remove tests for numericality matcher + integer column
* An integer column will typecast any non-integer value to an
integer.
* Because of the typecasting, our tests for the numericality matcher
against an integer column don't quite work, because we can't
really test what happens when the attribute is set to a
non-integer value. Now that `allow_value` is more strict, we're
getting a CouldNotSetAttributeError when attempting to do so.
* The tests mentioned were originally added to ensure that we are
handling RangeErrors that ActiveRecord used to emit. This doesn't
happen anymore, so the tests aren't necessary anymore either.
* Fix tests for acceptance matcher
* Fix tests for absence matcher
2015-09-26 05:10:00 +00:00
|
|
|
def ignoring_interference_by_writer?
|
|
|
|
@ignoring_interference_by_writer
|
|
|
|
end
|
|
|
|
|
2014-10-22 07:18:54 +00:00
|
|
|
def value_matches?(value)
|
|
|
|
self.value = value
|
|
|
|
set_attribute(value)
|
|
|
|
!(errors_match? || any_range_error_occurred?)
|
|
|
|
end
|
|
|
|
|
2015-01-21 22:46:26 +00:00
|
|
|
def set_attribute(value)
|
|
|
|
instance.__send__("#{attribute_to_set}=", value)
|
Tighten CouldNotSetAttributeError restriction
Why:
* Previously, `allow_value` would raise a CouldNotSetAttributeError
if the value being set didn't match the value the attribute had after
being set, but only if the attribute was being changed from nil to
non-nil or non-nil to nil.
* It turns out it doesn't matter which value you're trying to set the
attribute to -- if the attribute rejects that change it's confusing
either way. (In fact, I was recently bit by a case in which I was
trying to validate numericality of an attribute, where the writer
method for that attribute was overridden to ensure that the attribute
always stored a number and never contained non-number characters.
This ended up making the numericality validation useless, of
course -- but it caused confusion because the test acted in a way
I didn't expect.)
To satisfy the above:
* `allow_value` now raises a CouldNotSetAttributeError if the attribute
rejects the value being set in *any* way.
* However, add a `ignoring_interference_by_writer` qualifier so that
it is possible to manually override this behavior.
* Fix tests that are failing now because of this new change:
* Fix tests for allow_value matcher
* Fix tests for numericality matcher
* Remove tests for numericality matcher + integer column
* An integer column will typecast any non-integer value to an
integer.
* Because of the typecasting, our tests for the numericality matcher
against an integer column don't quite work, because we can't
really test what happens when the attribute is set to a
non-integer value. Now that `allow_value` is more strict, we're
getting a CouldNotSetAttributeError when attempting to do so.
* The tests mentioned were originally added to ensure that we are
handling RangeErrors that ActiveRecord used to emit. This doesn't
happen anymore, so the tests aren't necessary anymore either.
* Fix tests for acceptance matcher
* Fix tests for absence matcher
2015-09-26 05:10:00 +00:00
|
|
|
ensure_that_attribute_was_set!(value)
|
2015-07-23 22:19:47 +00:00
|
|
|
after_setting_value_callback.call
|
2015-01-21 22:46:26 +00:00
|
|
|
end
|
|
|
|
|
Tighten CouldNotSetAttributeError restriction
Why:
* Previously, `allow_value` would raise a CouldNotSetAttributeError
if the value being set didn't match the value the attribute had after
being set, but only if the attribute was being changed from nil to
non-nil or non-nil to nil.
* It turns out it doesn't matter which value you're trying to set the
attribute to -- if the attribute rejects that change it's confusing
either way. (In fact, I was recently bit by a case in which I was
trying to validate numericality of an attribute, where the writer
method for that attribute was overridden to ensure that the attribute
always stored a number and never contained non-number characters.
This ended up making the numericality validation useless, of
course -- but it caused confusion because the test acted in a way
I didn't expect.)
To satisfy the above:
* `allow_value` now raises a CouldNotSetAttributeError if the attribute
rejects the value being set in *any* way.
* However, add a `ignoring_interference_by_writer` qualifier so that
it is possible to manually override this behavior.
* Fix tests that are failing now because of this new change:
* Fix tests for allow_value matcher
* Fix tests for numericality matcher
* Remove tests for numericality matcher + integer column
* An integer column will typecast any non-integer value to an
integer.
* Because of the typecasting, our tests for the numericality matcher
against an integer column don't quite work, because we can't
really test what happens when the attribute is set to a
non-integer value. Now that `allow_value` is more strict, we're
getting a CouldNotSetAttributeError when attempting to do so.
* The tests mentioned were originally added to ensure that we are
handling RangeErrors that ActiveRecord used to emit. This doesn't
happen anymore, so the tests aren't necessary anymore either.
* Fix tests for acceptance matcher
* Fix tests for absence matcher
2015-09-26 05:10:00 +00:00
|
|
|
def ensure_that_attribute_was_set!(expected_value)
|
allow_value: Raise error if attr sets value differently
`allow_value` will now raise a CouldNotSetAttribute error if the
attribute in question cannot be changed from a non-nil value to a nil
value, or vice versa. In other words, these are the exact cases in which
the error will occur:
* If you're testing whether the attribute allows `nil`, but the
attribute detects and ignores nil. (For instance, you have a model
that `has_secure_password`. This will add a #password= method to your
model that is defined in a such a way that you cannot clear the
password by setting it to nil -- nothing happens.)
* If you're testing whether the attribute allows a non-nil value, but
the attribute fails to set that value. (For instance, you have an
ActiveRecord model. If ActiveRecord cannot typecast the value in the
context of the column, then it will do nothing, and the attribute will be
effectively set to nil.)
What's the reasoning behind this change? Simply put, if you are assuming
that the attribute is changing but in fact it is not, then the test
you're writing isn't the test that actually gets run. We feel that this
is dishonest and produces an invalid test.
2013-11-22 20:46:59 +00:00
|
|
|
actual_value = instance.__send__(attribute_to_set)
|
|
|
|
|
Tighten CouldNotSetAttributeError restriction
Why:
* Previously, `allow_value` would raise a CouldNotSetAttributeError
if the value being set didn't match the value the attribute had after
being set, but only if the attribute was being changed from nil to
non-nil or non-nil to nil.
* It turns out it doesn't matter which value you're trying to set the
attribute to -- if the attribute rejects that change it's confusing
either way. (In fact, I was recently bit by a case in which I was
trying to validate numericality of an attribute, where the writer
method for that attribute was overridden to ensure that the attribute
always stored a number and never contained non-number characters.
This ended up making the numericality validation useless, of
course -- but it caused confusion because the test acted in a way
I didn't expect.)
To satisfy the above:
* `allow_value` now raises a CouldNotSetAttributeError if the attribute
rejects the value being set in *any* way.
* However, add a `ignoring_interference_by_writer` qualifier so that
it is possible to manually override this behavior.
* Fix tests that are failing now because of this new change:
* Fix tests for allow_value matcher
* Fix tests for numericality matcher
* Remove tests for numericality matcher + integer column
* An integer column will typecast any non-integer value to an
integer.
* Because of the typecasting, our tests for the numericality matcher
against an integer column don't quite work, because we can't
really test what happens when the attribute is set to a
non-integer value. Now that `allow_value` is more strict, we're
getting a CouldNotSetAttributeError when attempting to do so.
* The tests mentioned were originally added to ensure that we are
handling RangeErrors that ActiveRecord used to emit. This doesn't
happen anymore, so the tests aren't necessary anymore either.
* Fix tests for acceptance matcher
* Fix tests for absence matcher
2015-09-26 05:10:00 +00:00
|
|
|
if expected_value != actual_value && !ignoring_interference_by_writer?
|
allow_value: Raise error if attr sets value differently
`allow_value` will now raise a CouldNotSetAttribute error if the
attribute in question cannot be changed from a non-nil value to a nil
value, or vice versa. In other words, these are the exact cases in which
the error will occur:
* If you're testing whether the attribute allows `nil`, but the
attribute detects and ignores nil. (For instance, you have a model
that `has_secure_password`. This will add a #password= method to your
model that is defined in a such a way that you cannot clear the
password by setting it to nil -- nothing happens.)
* If you're testing whether the attribute allows a non-nil value, but
the attribute fails to set that value. (For instance, you have an
ActiveRecord model. If ActiveRecord cannot typecast the value in the
context of the column, then it will do nothing, and the attribute will be
effectively set to nil.)
What's the reasoning behind this change? Simply put, if you are assuming
that the attribute is changing but in fact it is not, then the test
you're writing isn't the test that actually gets run. We feel that this
is dishonest and produces an invalid test.
2013-11-22 20:46:59 +00:00
|
|
|
raise CouldNotSetAttributeError.create(
|
|
|
|
instance.class,
|
|
|
|
attribute_to_set,
|
|
|
|
expected_value,
|
|
|
|
actual_value
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-03-29 20:10:01 +00:00
|
|
|
def errors_match?
|
|
|
|
has_messages? && errors_for_attribute_match?
|
2013-03-26 22:16:04 +00:00
|
|
|
end
|
|
|
|
|
2013-03-29 20:10:01 +00:00
|
|
|
def has_messages?
|
2015-01-21 22:46:26 +00:00
|
|
|
validator.has_messages?
|
2012-09-11 22:40:39 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def errors_for_attribute_match?
|
|
|
|
if expected_message
|
2013-03-28 18:48:45 +00:00
|
|
|
self.matched_error = errors_match_regexp? || errors_match_string?
|
2012-09-11 22:40:39 +00:00
|
|
|
else
|
|
|
|
errors_for_attribute.compact.any?
|
2010-09-05 15:23:09 +00:00
|
|
|
end
|
2010-12-15 22:34:19 +00:00
|
|
|
end
|
|
|
|
|
2012-03-23 14:10:08 +00:00
|
|
|
def errors_for_attribute
|
2015-01-21 22:46:26 +00:00
|
|
|
validator.formatted_messages
|
2012-09-11 22:40:39 +00:00
|
|
|
end
|
|
|
|
|
2010-12-15 22:34:19 +00:00
|
|
|
def errors_match_regexp?
|
2012-03-23 14:10:08 +00:00
|
|
|
if Regexp === expected_message
|
2012-05-07 14:25:46 +00:00
|
|
|
errors_for_attribute.detect { |e| e =~ expected_message }
|
2010-12-15 22:34:19 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def errors_match_string?
|
2012-03-23 14:10:08 +00:00
|
|
|
if errors_for_attribute.include?(expected_message)
|
2012-05-07 14:25:46 +00:00
|
|
|
expected_message
|
2010-12-15 22:34:19 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-01-21 22:46:26 +00:00
|
|
|
def any_range_error_occurred?
|
|
|
|
validator.captured_range_error?
|
|
|
|
end
|
|
|
|
|
2010-12-15 22:34:19 +00:00
|
|
|
def expectation
|
2014-10-22 07:17:58 +00:00
|
|
|
parts = [
|
2015-01-21 22:46:26 +00:00
|
|
|
expected_messages_description,
|
2014-10-22 07:17:58 +00:00
|
|
|
"when #{attribute_to_set} is set to #{value.inspect}"
|
|
|
|
]
|
|
|
|
|
|
|
|
parts.join(' ').squeeze(' ')
|
|
|
|
end
|
|
|
|
|
2015-01-21 22:46:26 +00:00
|
|
|
def expected_messages_description
|
|
|
|
validator.expected_messages_description(expected_message)
|
2010-12-15 22:34:19 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def error_description
|
2015-01-21 22:46:26 +00:00
|
|
|
validator.messages_description
|
2012-09-11 22:40:39 +00:00
|
|
|
end
|
|
|
|
|
2012-03-23 14:10:08 +00:00
|
|
|
def allowed_values
|
2013-03-28 18:48:45 +00:00
|
|
|
if values_to_match.length > 1
|
|
|
|
"any of [#{values_to_match.map(&:inspect).join(', ')}]"
|
2012-03-23 14:10:08 +00:00
|
|
|
else
|
2013-03-28 18:48:45 +00:00
|
|
|
values_to_match.first.inspect
|
2012-03-23 14:10:08 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def expected_message
|
2013-03-28 18:48:45 +00:00
|
|
|
if options.key?(:expected_message)
|
|
|
|
if Symbol === options[:expected_message]
|
2012-09-12 00:53:21 +00:00
|
|
|
default_expected_message
|
2012-03-23 14:10:08 +00:00
|
|
|
else
|
2013-03-28 18:48:45 +00:00
|
|
|
options[:expected_message]
|
2012-03-23 14:10:08 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-09-12 00:53:21 +00:00
|
|
|
def default_expected_message
|
2015-01-21 22:46:26 +00:00
|
|
|
validator.expected_message_from(default_attribute_message)
|
2012-09-12 00:53:21 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def default_attribute_message
|
2013-02-14 22:25:46 +00:00
|
|
|
default_error_message(
|
2013-03-28 18:48:45 +00:00
|
|
|
options[:expected_message],
|
2014-04-26 15:10:17 +00:00
|
|
|
default_attribute_message_values
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
def default_attribute_message_values
|
|
|
|
defaults = {
|
2014-01-17 20:20:44 +00:00
|
|
|
model_name: model_name,
|
|
|
|
instance: instance,
|
2014-10-15 06:13:17 +00:00
|
|
|
attribute: attribute_to_check_message_against,
|
2014-04-26 15:10:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
defaults.merge(options[:expected_message_values])
|
2012-09-12 00:53:21 +00:00
|
|
|
end
|
|
|
|
|
2012-03-23 14:10:08 +00:00
|
|
|
def model_name
|
2013-03-28 18:48:45 +00:00
|
|
|
instance.class.to_s.underscore
|
2012-03-23 14:10:08 +00:00
|
|
|
end
|
|
|
|
end
|
2010-12-15 22:34:19 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|