Handle RangeErrors emitted now in ActiveRecord 4.2

In Rails 4.2, ActiveRecord was changed such that if you attempt to set
an attribute to a value and that value is outside the range of the
column, then it will raise a RangeError. For instance, an integer column
with a limit of 2 (i.e. a smallint) only accepts values between -32768
and +32767.

This means that if you try to do any of these three things, a RangeError
could be raised:

* Use validate_numericality_of along with any of the comparison
  submatchers and a value that sits on either side of the boundary.
* Use allow_value with a value that sits outside the range.
* Use validates_inclusion_of against an integer column. (Here we attempt
  to set that column to a non-integer value to verify that the attribute
  does not allow said value. That value is really a string version of a
  large number, so if the column does not take large numbers then the
  matcher could blow up.)

Ancillary changes in this commit:

* Remove ValidationMessageFinder and ExceptionMessageFinder in favor of
  Validator, StrictValidator, and ValidatorWithCapturedRangeError.
* The allow_value matcher now uses an instance of Validator under the
  hood. StrictValidator and/or ValidatorWithCapturedRangeError may be
  mixed into the Validator object as needed.
This commit is contained in:
Elliot Winkler 2015-01-21 15:46:26 -07:00
parent 1766a09299
commit 9ba21381d7
22 changed files with 550 additions and 438 deletions

10
NEWS.md
View File

@ -8,7 +8,17 @@
the association has (or has not) been declared with *any* dependent option.
([#631])
* Fix `allow_value`, `validate_numericality_of` and `validate_inclusion_of` so
that they handle RangeErrors emitted from ActiveRecord 4.2. These exceptions
arise whenever we attempt to set an attribute using a value that lies outside
the range of the column (assuming the column is an integer). RangeError is now
treated specially, failing the test instead of bubbling up as an error.
([#634], [#637], [#642])
[#631]: https://github.com/thoughtbot/shoulda-matchers/pull/631
[#634]: https://github.com/thoughtbot/shoulda-matchers/pull/634
[#637]: https://github.com/thoughtbot/shoulda-matchers/pull/637
[#642]: https://github.com/thoughtbot/shoulda-matchers/pull/642
# 2.8.0.rc1

View File

@ -1,7 +1,8 @@
require 'shoulda/matchers/active_model/helpers'
require 'shoulda/matchers/active_model/validation_matcher'
require 'shoulda/matchers/active_model/validation_message_finder'
require 'shoulda/matchers/active_model/exception_message_finder'
require 'shoulda/matchers/active_model/validator'
require 'shoulda/matchers/active_model/strict_validator'
require 'shoulda/matchers/active_model/validator_with_captured_range_error'
require 'shoulda/matchers/active_model/allow_value_matcher'
require 'shoulda/matchers/active_model/disallow_value_matcher'
require 'shoulda/matchers/active_model/validate_length_of_matcher'

View File

@ -177,9 +177,9 @@ module Shoulda
def initialize(*values)
self.values_to_match = values
self.message_finder_factory = ValidationMessageFinder
self.options = {}
self.after_setting_value_callback = -> {}
self.validator = Validator.new
end
def for(attribute)
@ -189,7 +189,7 @@ module Shoulda
end
def on(context)
@context = context
validator.context = context
self
end
@ -205,7 +205,7 @@ module Shoulda
end
def strict
self.message_finder_factory = ExceptionMessageFinder
validator.strict = true
self
end
@ -215,16 +215,18 @@ module Shoulda
def matches?(instance)
self.instance = instance
validator.record = instance
values_to_match.none? do |value|
validator.reset
self.value = value
set_value(value)
errors_match?
set_attribute(value)
errors_match? || any_range_error_occurred?
end
end
def failure_message
"Did not expect #{expectation},\ngot error: #{matched_error}"
"Did not expect #{expectation},\ngot#{error_description}"
end
alias failure_message_for_should failure_message
@ -234,26 +236,45 @@ module Shoulda
alias failure_message_for_should_not failure_message_when_negated
def description
message_finder.allow_description(allowed_values)
validator.allow_description(allowed_values)
end
protected
attr_accessor :values_to_match, :message_finder_factory,
:instance, :attribute_to_set, :attribute_to_check_message_against,
:context, :value, :matched_error, :after_setting_value_callback
attr_reader :attribute_to_check_message_against
attr_accessor :values_to_match, :instance, :attribute_to_set, :value,
:matched_error, :after_setting_value_callback, :validator
def set_value(value)
instance.__send__("#{attribute_to_set}=", value)
def attribute_to_check_message_against=(attribute)
@attribute_to_check_message_against = attribute
validator.attribute = attribute
end
def set_attribute(value)
set_attribute_ignoring_range_errors(value)
after_setting_value_callback.call
end
def set_attribute_ignoring_range_errors(value)
instance.__send__("#{attribute_to_set}=", value)
rescue RangeError => exception
# Have to reset the attribute so that we don't get a RangeError the
# next time we attempt to write the attribute (ActiveRecord seems to
# set the attribute to the "bad" value anyway)
reset_attribute
validator.capture_range_error(exception)
end
def reset_attribute
instance.send(:raw_write_attribute, attribute_to_set, nil)
end
def errors_match?
has_messages? && errors_for_attribute_match?
end
def has_messages?
message_finder.has_messages?
validator.has_messages?
end
def errors_for_attribute_match?
@ -265,7 +286,7 @@ module Shoulda
end
def errors_for_attribute
message_finder.messages
validator.formatted_messages
end
def errors_match_regexp?
@ -280,30 +301,25 @@ module Shoulda
end
end
def any_range_error_occurred?
validator.captured_range_error?
end
def expectation
parts = [
error_source,
includes_expected_message,
expected_messages_description,
"when #{attribute_to_set} is set to #{value.inspect}"
]
parts.join(' ').squeeze(' ')
end
def includes_expected_message
if expected_message
"to include #{expected_message.inspect}"
else
''
end
end
def error_source
message_finder.source_description
def expected_messages_description
validator.expected_messages_description(expected_message)
end
def error_description
message_finder.messages_description
validator.messages_description
end
def allowed_values
@ -325,7 +341,7 @@ module Shoulda
end
def default_expected_message
message_finder.expected_message_from(default_attribute_message)
validator.expected_message_from(default_attribute_message)
end
def default_attribute_message
@ -348,10 +364,6 @@ module Shoulda
def model_name
instance.class.to_s.underscore
end
def message_finder
message_finder_factory.new(instance, attribute_to_check_message_against, context)
end
end
end
end

View File

@ -1,58 +0,0 @@
module Shoulda
module Matchers
module ActiveModel
# @private
class ExceptionMessageFinder
def initialize(instance, attribute, context=nil)
@instance = instance
@attribute = attribute
@context = context
end
def allow_description(allowed_values)
"doesn't raise when #{@attribute} is set to #{allowed_values}"
end
def messages_description
if has_messages?
": #{messages.join.inspect}"
else
' no exception'
end
end
def has_messages?
messages.any?
end
def messages
@messages ||= validate_and_rescue
end
def source_description
'exception'
end
def expected_message_from(attribute_message)
"#{human_attribute_name} #{attribute_message}"
end
private
def validate_and_rescue
@instance.valid?(@context)
[]
rescue ::ActiveModel::StrictValidationFailed => exception
[exception.message]
end
def human_attribute_name
@instance.class.human_attribute_name(@attribute)
end
end
end
end
end

View File

@ -24,32 +24,18 @@ module Shoulda
end.join("\n")
end
# Helper method that determines the default error message used by Active
# Record. Works for both existing Rails 2.1 and Rails 2.2 with the newly
# introduced I18n module used for localization. Use with Rails 3.0 and
# up will delegate to ActiveModel::Errors.generate_error if a model
# instance is given.
#
# default_error_message(:blank)
# default_error_message(:too_short, count: 5)
# default_error_message(:too_long, count: 60)
# default_error_message(:blank, model_name: 'user', attribute: 'name')
# default_error_message(:blank, instance: #<Model>, attribute: 'name')
def default_error_message(key, options = {})
def default_error_message(type, options = {})
model_name = options.delete(:model_name)
attribute = options.delete(:attribute)
instance = options.delete(:instance)
if instance && instance.errors.respond_to?(:generate_message)
instance.errors.generate_message(attribute.to_sym, key, options)
else
default_translation = [ :"activerecord.errors.models.#{model_name}.#{key}",
:"activerecord.errors.messages.#{key}",
:"errors.attributes.#{attribute}.#{key}",
:"errors.messages.#{key}" ]
I18n.translate(:"activerecord.errors.models.#{model_name}.attributes.#{attribute}.#{key}",
{ default: default_translation }.merge(options))
end
RailsShim.generate_validation_message(
instance,
attribute.to_sym,
type,
model_name,
options
)
end
end
end

View File

@ -100,16 +100,16 @@ module Shoulda
def assertions
case @operator
when :>
[false, false, true]
when :>=
[false, true, true]
when :==
[false, true, false]
when :<
[true, false, false]
when :<=
[true, true, false]
when :>
[false, false, true]
when :>=
[false, true, true]
when :==
[false, true, false]
when :<
[true, false, false]
when :<=
[true, true, false]
end
end

View File

@ -0,0 +1,50 @@
module Shoulda
module Matchers
module ActiveModel
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

View File

@ -1,69 +0,0 @@
module Shoulda
module Matchers
module ActiveModel
# @private
class ValidationMessageFinder
include Helpers
def initialize(instance, attribute, context=nil)
@instance = instance
@attribute = attribute
@context = context
end
def allow_description(allowed_values)
"allow #{@attribute} to be set to #{allowed_values}"
end
def expected_message_from(attribute_message)
attribute_message
end
def has_messages?
errors.present?
end
def source_description
'errors'
end
def messages_description
if errors.empty?
' no errors'
else
" errors:\n#{pretty_error_messages(validated_instance)}"
end
end
def messages
Array(messages_for_attribute)
end
private
def messages_for_attribute
if errors.respond_to?(:[])
errors[@attribute]
else
errors.on(@attribute)
end
end
def errors
validated_instance.errors
end
def validated_instance
@validated_instance ||= validate_instance
end
def validate_instance
@instance.valid?(*@context)
@instance
end
end
end
end
end

View File

@ -0,0 +1,113 @@
module Shoulda
module Matchers
module ActiveModel
# @private
class Validator
include Helpers
attr_writer :attribute, :context, :record
def initialize
reset
end
def reset
@messages = nil
end
def strict=(strict)
@strict = strict
if strict
extend StrictValidator
end
end
def capture_range_error(exception)
@captured_range_error = exception
extend ValidatorWithCapturedRangeError
end
def allow_description(allowed_values)
"allow #{attribute} to be set to #{allowed_values}"
end
def expected_message_from(attribute_message)
attribute_message
end
def messages
@messages ||= collect_messages
end
def formatted_messages
messages
end
def has_messages?
messages.any?
end
def messages_description
if has_messages?
" errors:\n#{pretty_error_messages(record)}"
else
' no errors'
end
end
def expected_messages_description(expected_message)
if expected_message
"errors to include #{expected_message.inspect}"
else
'errors'
end
end
def captured_range_error?
!!captured_range_error
end
protected
attr_reader :attribute, :context, :strict, :record,
:captured_range_error
def collect_messages
validation_errors
end
private
def strict?
!!@strict
end
def collect_errors_or_exceptions
collect_messages
rescue RangeError => exception
capture_range_error(exception)
[]
end
def validation_errors
if context
record.valid?(context)
else
record.valid?
end
if record.errors.respond_to?(:[])
record.errors[attribute]
else
record.errors.on(attribute)
end
end
def human_attribute_name
record.class.human_attribute_name(attribute)
end
end
end
end
end

View File

@ -0,0 +1,11 @@
module Shoulda
module Matchers
module ActiveModel
module ValidatorWithCapturedRangeError
def messages_description
' RangeError: ' + captured_range_error.message.inspect
end
end
end
end
end

View File

@ -65,6 +65,28 @@ module Shoulda
end
end
def self.generate_validation_message(record, attribute, type, model_name, options)
if record && record.errors.respond_to?(:generate_message)
record.errors.generate_message(attribute.to_sym, type, options)
else
simply_generate_validation_message(attribute, type, model_name, options)
end
rescue RangeError
simply_generate_validation_message(attribute, type, model_name, options)
end
def self.simply_generate_validation_message(attribute, type, model_name, options)
default_translation_keys = [
:"activerecord.errors.models.#{model_name}.#{type}",
:"activerecord.errors.messages.#{type}",
:"errors.attributes.#{attribute}.#{type}",
:"errors.messages.#{type}"
]
primary_translation_key = :"activerecord.errors.models.#{model_name}.attributes.#{attribute}.#{type}"
translate_options = { default: default_translation_keys }.merge(options)
I18n.translate(primary_translation_key, translate_options)
end
def self.active_record_major_version
::ActiveRecord::VERSION::MAJOR
end

View File

@ -7,8 +7,10 @@ module UnitTests
def custom_validation(options = {}, &block)
attribute_name = options.fetch(:attribute_name, :attr)
attribute_type = options.fetch(:attribute_type, :integer)
column_options = options.fetch(:column_options, {})
attribute_options = { type: attribute_type, options: column_options }
define_model(:example, attribute_name => attribute_type) do
define_model(:example, attribute_name => attribute_options) do
validate :custom_validation
define_method(:custom_validation, &block)

View File

@ -5,6 +5,10 @@ module UnitTests
example_group.extend(self)
end
def active_model_version
Tests::Version.new(::ActiveModel::VERSION::STRING)
end
def active_model_3_1?
(::ActiveModel::VERSION::MAJOR == 3 && ::ActiveModel::VERSION::MINOR >= 1) || active_model_4_0?
end
@ -16,5 +20,9 @@ module UnitTests
def active_model_4_0?
::ActiveModel::VERSION::MAJOR == 4
end
def active_model_supports_strict?
active_model_version >= 3.2
end
end
end

View File

@ -0,0 +1,16 @@
module UnitTests
module ActiveRecordVersions
def self.configure_example_group(example_group)
example_group.include(self)
example_group.extend(self)
end
def active_record_version
Tests::Version.new(ActiveRecord::VERSION::STRING)
end
def active_record_can_raise_range_error?
active_record_version >= 4.2
end
end
end

View File

@ -6,19 +6,19 @@ module UnitTests
end
def rails_version
Gem::Version.new(Rails::VERSION::STRING)
Tests::Version.new(Rails::VERSION::STRING)
end
def rails_3_x?
Gem::Requirement.new('~> 3.0').satisfied_by?(rails_version)
rails_version =~ '~> 3.0'
end
def rails_4_x?
Gem::Requirement.new('~> 4.0').satisfied_by?(rails_version)
rails_version =~ '~> 4.0'
end
def rails_gte_4_1?
Gem::Requirement.new('>= 4.1').satisfied_by?(rails_version)
rails_version >= 4.1
end
def active_record_supports_enum?

View File

@ -227,4 +227,78 @@ describe Shoulda::Matchers::ActiveModel::AllowValueMatcher, type: :model do
end
end
end
if active_record_can_raise_range_error?
context 'when the value is outside of the range of the column' do
context 'not qualified with strict' do
it 'rejects, failing with the correct message' do
attribute_options = { type: :integer, options: { limit: 2 } }
record = define_model(:example, attr: attribute_options).new
assertion = -> { expect(record).to allow_value(100000).for(:attr) }
message = <<-MESSAGE.strip_heredoc.strip
Did not expect errors when attr is set to 100000,
got RangeError: "100000 is out of range for ActiveRecord::Type::Integer with limit 2"
MESSAGE
expect(&assertion).to fail_with_message(message)
end
context 'qualified with a message' do
it 'ignores any specified message, failing with the correct message' do
attribute_options = { type: :integer, options: { limit: 2 } }
record = define_model(:example, attr: attribute_options).new
assertion = -> do
expect(record).
to allow_value(100000).
for(:attr).
with_message('some message')
end
message = <<-MESSAGE.strip_heredoc.strip
Did not expect errors to include "some message" when attr is set to 100000,
got RangeError: "100000 is out of range for ActiveRecord::Type::Integer with limit 2"
MESSAGE
expect(&assertion).to fail_with_message(message)
end
end
end
if active_model_supports_strict?
context 'qualified with strict' do
it 'rejects, failing with the correct message' do
attribute_options = { type: :integer, options: { limit: 2 } }
record = define_model(:example, attr: attribute_options).new
assertion = -> do
expect(record).
to allow_value(100000).
for(:attr).
strict
end
message = <<-MESSAGE.strip_heredoc.strip
Did not expect an exception to have been raised when attr is set to 100000,
got RangeError: "100000 is out of range for ActiveRecord::Type::Integer with limit 2"
MESSAGE
expect(&assertion).to fail_with_message(message)
end
context 'qualified with a message' do
it 'ignores any specified message' do
attribute_options = { type: :integer, options: { limit: 2 } }
record = define_model(:example, attr: attribute_options).new
assertion = -> do
expect(record).
to allow_value(100000).
for(:attr).
with_message('some message').
strict
end
message = <<-MESSAGE.strip_heredoc.strip
Did not expect exception to include "some message" when attr is set to 100000,
got RangeError: "100000 is out of range for ActiveRecord::Type::Integer with limit 2"
MESSAGE
expect(&assertion).to fail_with_message(message)
end
end
end
end
end
end
end

View File

@ -79,7 +79,82 @@ describe Shoulda::Matchers::ActiveModel::DisallowValueMatcher, type: :model do
end
end
if active_record_can_raise_range_error?
context 'when the value is outside of the range of the column' do
context 'not qualified with strict' do
it 'accepts, failing with the correct message' do
attribute_options = { type: :integer, options: { limit: 2 } }
record = define_model(:example, attr: attribute_options).new
assertion = -> { expect(record).not_to disallow_value(100000).for(:attr) }
message = <<-MESSAGE.strip_heredoc.strip
Did not expect errors when attr is set to 100000,
got RangeError: "100000 is out of range for ActiveRecord::Type::Integer with limit 2"
MESSAGE
expect(&assertion).to fail_with_message(message)
end
context 'qualified with a message' do
it 'ignores any specified message, failing with the correct message' do
attribute_options = { type: :integer, options: { limit: 2 } }
record = define_model(:example, attr: attribute_options).new
assertion = -> do
expect(record).
not_to disallow_value(100000).
for(:attr).
with_message('some message')
end
message = <<-MESSAGE.strip_heredoc.strip
Did not expect errors to include "some message" when attr is set to 100000,
got RangeError: "100000 is out of range for ActiveRecord::Type::Integer with limit 2"
MESSAGE
expect(&assertion).to fail_with_message(message)
end
end
end
if active_model_supports_strict?
context 'qualified with strict' do
it 'accepts, failing with the correct message' do
attribute_options = { type: :integer, options: { limit: 2 } }
record = define_model(:example, attr: attribute_options).new
assertion = -> do
expect(record).
not_to disallow_value(100000).
for(:attr).
strict
end
message = <<-MESSAGE.strip_heredoc.strip
Did not expect an exception to have been raised when attr is set to 100000,
got RangeError: "100000 is out of range for ActiveRecord::Type::Integer with limit 2"
MESSAGE
expect(&assertion).to fail_with_message(message)
end
context 'qualified with a message' do
it 'ignores any specified message' do
attribute_options = { type: :integer, options: { limit: 2 } }
record = define_model(:example, attr: attribute_options).new
assertion = -> do
expect(record).
not_to disallow_value(100000).
for(:attr).
with_message('some message').
strict
end
message = <<-MESSAGE.strip_heredoc.strip
Did not expect exception to include "some message" when attr is set to 100000,
got RangeError: "100000 is out of range for ActiveRecord::Type::Integer with limit 2"
MESSAGE
expect(&assertion).to fail_with_message(message)
end
end
end
end
end
end
def matcher(value)
described_class.new(value)
end
alias_method :disallow_value, :matcher
end

View File

@ -1,111 +0,0 @@
require 'unit_spec_helper'
describe Shoulda::Matchers::ActiveModel::ExceptionMessageFinder, type: :model do
if active_model_3_2?
context '#allow_description' do
it 'describes its attribute' do
finder = build_finder(attribute: :attr)
description = finder.allow_description('allowed values')
expect(description).to eq %q(doesn't raise when attr is set to allowed values)
end
end
context '#expected_message_from' do
it 'returns the message with the attribute name prefixed' do
finder = build_finder(attribute: :attr)
message = finder.expected_message_from('some message')
expect(message).to eq 'Attr some message'
end
end
context '#has_messages?' do
it 'has messages when some validations fail' do
finder = build_finder(format: /abc/, value: 'xyz')
result = finder.has_messages?
expect(result).to eq true
end
it 'has no messages when all validations pass' do
finder = build_finder(format: /abc/, value: 'abc')
result = finder.has_messages?
expect(result).to eq false
end
end
context '#messages' do
it 'returns errors for the given attribute' do
finder = build_finder(
attribute: :attr,
format: /abc/,
value: 'xyz'
)
messages = finder.messages
expect(messages).to eq ['Attr is invalid']
end
end
context '#messages_description' do
it 'describes errors for the given attribute' do
finder = build_finder(
attribute: :attr,
format: /abc/,
value: 'xyz'
)
description = finder.messages_description
expect(description).to eq ': "Attr is invalid"'
end
it 'describes errors when there are none' do
finder = build_finder(format: /abc/, value: 'abc')
description = finder.messages_description
expect(description).to eq ' no exception'
end
end
context '#source_description' do
it 'describes the source of its messages' do
finder = build_finder
description = finder.source_description
expect(description).to eq 'exception'
end
end
end
def build_finder(arguments = {})
arguments[:attribute] ||= :attr
instance = build_instance_validating(
arguments[:attribute],
arguments[:format] || /abc/,
arguments[:value] || 'abc'
)
Shoulda::Matchers::ActiveModel::ExceptionMessageFinder.new(
instance,
arguments[:attribute]
)
end
def build_instance_validating(attribute, format, value)
model_class = define_model(:example, attribute => :string) do
attr_accessible attribute
validates_format_of attribute, with: format, strict: true
end
model_class.new(attribute => value)
end
end

View File

@ -68,6 +68,30 @@ describe Shoulda::Matchers::ActiveModel::ValidateInclusionOfMatcher, type: :mode
end
end
context 'against an attribute with a specific column limit' do
it 'does not raise an exception when attempting to use the matcher' do
possible_values = (1..5).to_a
builder = build_object_allowing(possible_values)
assertion = -> { expect_to_match_on_values(builder, possible_values) }
expect(&assertion).not_to raise_error
end
def build_object(options = {}, &block)
build_object_with_generic_attribute(
options.merge(
column_type: :integer,
column_options: { limit: 2 },
value: 1
),
&block
)
end
def expect_to_match_on_values(builder, values, &block)
expect_to_match_in_array(builder, values, &block)
end
end
context "against a float attribute" do
it_behaves_like 'it supports in_array',
possible_values: [1.0, 2.0, 3.0, 4.0, 5.0],

View File

@ -123,6 +123,72 @@ describe Shoulda::Matchers::ActiveModel::ValidateNumericalityOfMatcher, type: :m
end
end
context 'qualified with less_than_or_equal_to' do
it 'does not raise an error if the given value is right at the allowed max value for the column' do
record = record_with_integer_column_of_limit(:attr, 2, less_than_or_equal_to: 32767)
assertion = -> {
expect(record).to validate_numericality_of(:attr).is_less_than_or_equal_to(32767)
}
expect(&assertion).not_to raise_error
end
end
context 'qualified with less_than' do
it 'does not raise an error if the given value is right at the allowed max value for the column' do
record = record_with_integer_column_of_limit(:attr, 2, less_than: 32767)
assertion = -> {
expect(record).to validate_numericality_of(:attr).is_less_than(32767)
}
expect(&assertion).not_to raise_error
end
end
context 'qualified with equal_to' do
it 'does not raise an error if the given value is right at the allowed min value for the column' do
record = record_with_integer_column_of_limit(:attr, 2, equal_to: -32768)
assertion = -> {
expect(record).to validate_numericality_of(:attr).is_equal_to(-32768)
}
expect(&assertion).not_to raise_error
end
it 'does not raise an error if the given value is right at the allowed max value for the column' do
record = record_with_integer_column_of_limit(:attr, 2, equal_to: 32767)
assertion = -> {
expect(record).to validate_numericality_of(:attr).is_equal_to(32767)
}
expect(&assertion).not_to raise_error
end
end
context 'qualified with greater_than_or_equal to' do
it 'does not raise an error if the given value is right at the allowed min value for the column' do
record = record_with_integer_column_of_limit(:attr, 2,
greater_than_or_equal_to: -32768
)
assertion = -> {
expect(record).
to validate_numericality_of(:attr).
is_greater_than_or_equal_to(-32768)
}
expect(&assertion).not_to raise_error
end
end
context 'qualified with greater_than' do
it 'does not raise an error if the given value is right at the allowed min value for the column' do
record = record_with_integer_column_of_limit(:attr, 2,
greater_than: -32768
)
assertion = -> {
expect(record).
to validate_numericality_of(:attr).
is_greater_than(-32768)
}
expect(&assertion).not_to raise_error
end
end
context 'with multiple options together' do
context 'the success cases' do
it do
@ -349,4 +415,11 @@ describe Shoulda::Matchers::ActiveModel::ValidateNumericalityOfMatcher, type: :m
def matcher
validate_numericality_of(:attr)
end
def record_with_integer_column_of_limit(attribute, limit, validation_options = {})
column_options = { type: :integer, options: { limit: limit } }
define_model :example, attribute => column_options do
validates_numericality_of attribute, validation_options
end.new
end
end

View File

@ -1,129 +0,0 @@
require 'unit_spec_helper'
describe Shoulda::Matchers::ActiveModel::ValidationMessageFinder do
context '#allow_description' do
it 'describes its attribute' do
finder = build_finder(attribute: :attr)
description = finder.allow_description('allowed values')
expect(description).to eq 'allow attr to be set to allowed values'
end
end
context '#expected_message_from' do
it 'returns the message as-is' do
finder = build_finder
message = finder.expected_message_from('some message')
expect(message).to eq 'some message'
end
end
context '#has_messages?' do
it 'has messages when some validations fail' do
finder = build_finder(format: /abc/, value: 'xyz')
result = finder.has_messages?
expect(result).to eq true
end
it 'has no messages when all validations pass' do
finder = build_finder(format: /abc/, value: 'abc')
result = finder.has_messages?
expect(result).to eq false
end
end
context '#messages' do
it 'returns errors for the given attribute' do
finder = build_finder(format: /abc/, value: 'xyz')
messages = finder.messages
expect(messages).to eq ['is invalid']
end
it 'returns an empty array if there are no errors for the given attribute' do
finder = build_finder
messages = finder.messages
expect(messages).to eq([])
end
end
context '#messages_description' do
it 'describes errors for the given attribute' do
finder = build_finder(
attribute: :attr,
format: /abc/,
value: 'xyz'
)
description = finder.messages_description
expect(description).to eq(
%{ errors:\n* "is invalid" (attribute: attr, value: "xyz")}
)
end
it 'describes errors when there are none' do
finder = build_finder(format: /abc/, value: 'abc')
description = finder.messages_description
expect(description).to eq ' no errors'
end
it 'should not fetch attribute values for errors that were copied from an autosaved belongs_to association' do
instance = define_model(:example) do
validate do |record|
record.errors.add('association.association_attribute', 'is invalid')
end
end.new
finder = Shoulda::Matchers::ActiveModel::ValidationMessageFinder.new(instance, :attribute)
expect(finder.messages_description).to eq(
%{ errors:\n* "is invalid" (attribute: association.association_attribute)}
)
end
end
context '#source_description' do
it 'describes the source of its messages' do
finder = build_finder
description = finder.source_description
expect(description).to eq 'errors'
end
end
def build_finder(arguments = {})
arguments[:attribute] ||= :attr
instance = build_instance_validating(
arguments[:attribute],
arguments[:format] || /abc/,
arguments[:value] || 'abc'
)
Shoulda::Matchers::ActiveModel::ValidationMessageFinder.new(
instance,
arguments[:attribute]
)
end
def build_instance_validating(attribute, format, value)
model_class = define_model(:example, attribute => :string) do
attr_accessible attribute
validates_format_of attribute, with: format
end
model_class.new(attribute => value)
end
end

View File

@ -58,6 +58,8 @@ RSpec.configure do |config|
UnitTests::MailerBuilder.configure_example_group(config)
UnitTests::ModelBuilder.configure_example_group(config)
UnitTests::RailsVersions.configure_example_group(config)
UnitTests::ActiveRecordVersions.configure_example_group(config)
UnitTests::ActiveModelVersions.configure_example_group(config)
config.include UnitTests::Matchers
end