From 9ba21381d7caf045053a81f32df7de2f49687820 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Wed, 21 Jan 2015 15:46:26 -0700 Subject: [PATCH] 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. --- NEWS.md | 10 ++ lib/shoulda/matchers/active_model.rb | 5 +- .../active_model/allow_value_matcher.rb | 76 ++++++----- .../active_model/exception_message_finder.rb | 58 -------- lib/shoulda/matchers/active_model/helpers.rb | 30 ++-- .../comparison_matcher.rb | 20 +-- .../matchers/active_model/strict_validator.rb | 50 +++++++ .../active_model/validation_message_finder.rb | 69 ---------- .../matchers/active_model/validator.rb | 113 +++++++++++++++ .../validator_with_captured_range_error.rb | 11 ++ lib/shoulda/matchers/rails_shim.rb | 22 +++ .../unit/helpers/active_model_helpers.rb | 4 +- .../unit/helpers/active_model_versions.rb | 8 ++ .../unit/helpers/active_record_versions.rb | 16 +++ spec/support/unit/helpers/rails_versions.rb | 8 +- .../active_model/allow_value_matcher_spec.rb | 74 ++++++++++ .../disallow_value_matcher_spec.rb | 75 ++++++++++ .../exception_message_finder_spec.rb | 111 --------------- .../validate_inclusion_of_matcher_spec.rb | 24 ++++ .../validate_numericality_of_matcher_spec.rb | 73 ++++++++++ .../validation_message_finder_spec.rb | 129 ------------------ spec/unit_spec_helper.rb | 2 + 22 files changed, 550 insertions(+), 438 deletions(-) delete mode 100644 lib/shoulda/matchers/active_model/exception_message_finder.rb create mode 100644 lib/shoulda/matchers/active_model/strict_validator.rb delete mode 100644 lib/shoulda/matchers/active_model/validation_message_finder.rb create mode 100644 lib/shoulda/matchers/active_model/validator.rb create mode 100644 lib/shoulda/matchers/active_model/validator_with_captured_range_error.rb create mode 100644 spec/support/unit/helpers/active_record_versions.rb delete mode 100644 spec/unit/shoulda/matchers/active_model/exception_message_finder_spec.rb delete mode 100644 spec/unit/shoulda/matchers/active_model/validation_message_finder_spec.rb diff --git a/NEWS.md b/NEWS.md index 85de8010..27233b00 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 diff --git a/lib/shoulda/matchers/active_model.rb b/lib/shoulda/matchers/active_model.rb index ec439747..c5c2f430 100644 --- a/lib/shoulda/matchers/active_model.rb +++ b/lib/shoulda/matchers/active_model.rb @@ -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' diff --git a/lib/shoulda/matchers/active_model/allow_value_matcher.rb b/lib/shoulda/matchers/active_model/allow_value_matcher.rb index af069bae..de540d81 100644 --- a/lib/shoulda/matchers/active_model/allow_value_matcher.rb +++ b/lib/shoulda/matchers/active_model/allow_value_matcher.rb @@ -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 diff --git a/lib/shoulda/matchers/active_model/exception_message_finder.rb b/lib/shoulda/matchers/active_model/exception_message_finder.rb deleted file mode 100644 index 84b1e4dc..00000000 --- a/lib/shoulda/matchers/active_model/exception_message_finder.rb +++ /dev/null @@ -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 - - diff --git a/lib/shoulda/matchers/active_model/helpers.rb b/lib/shoulda/matchers/active_model/helpers.rb index 1a859e21..9eaae1d6 100644 --- a/lib/shoulda/matchers/active_model/helpers.rb +++ b/lib/shoulda/matchers/active_model/helpers.rb @@ -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: #, 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 diff --git a/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb b/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb index 7575c8fb..40090eb0 100644 --- a/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +++ b/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb @@ -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 diff --git a/lib/shoulda/matchers/active_model/strict_validator.rb b/lib/shoulda/matchers/active_model/strict_validator.rb new file mode 100644 index 00000000..b18f7cb5 --- /dev/null +++ b/lib/shoulda/matchers/active_model/strict_validator.rb @@ -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 diff --git a/lib/shoulda/matchers/active_model/validation_message_finder.rb b/lib/shoulda/matchers/active_model/validation_message_finder.rb deleted file mode 100644 index 90adfb51..00000000 --- a/lib/shoulda/matchers/active_model/validation_message_finder.rb +++ /dev/null @@ -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 - diff --git a/lib/shoulda/matchers/active_model/validator.rb b/lib/shoulda/matchers/active_model/validator.rb new file mode 100644 index 00000000..666fb6a8 --- /dev/null +++ b/lib/shoulda/matchers/active_model/validator.rb @@ -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 diff --git a/lib/shoulda/matchers/active_model/validator_with_captured_range_error.rb b/lib/shoulda/matchers/active_model/validator_with_captured_range_error.rb new file mode 100644 index 00000000..d30140a0 --- /dev/null +++ b/lib/shoulda/matchers/active_model/validator_with_captured_range_error.rb @@ -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 diff --git a/lib/shoulda/matchers/rails_shim.rb b/lib/shoulda/matchers/rails_shim.rb index 391fd971..1ef422ba 100644 --- a/lib/shoulda/matchers/rails_shim.rb +++ b/lib/shoulda/matchers/rails_shim.rb @@ -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 diff --git a/spec/support/unit/helpers/active_model_helpers.rb b/spec/support/unit/helpers/active_model_helpers.rb index f2f95e26..4f5bbc79 100644 --- a/spec/support/unit/helpers/active_model_helpers.rb +++ b/spec/support/unit/helpers/active_model_helpers.rb @@ -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) diff --git a/spec/support/unit/helpers/active_model_versions.rb b/spec/support/unit/helpers/active_model_versions.rb index 8bc5686a..ccbcda61 100644 --- a/spec/support/unit/helpers/active_model_versions.rb +++ b/spec/support/unit/helpers/active_model_versions.rb @@ -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 diff --git a/spec/support/unit/helpers/active_record_versions.rb b/spec/support/unit/helpers/active_record_versions.rb new file mode 100644 index 00000000..9e2202d8 --- /dev/null +++ b/spec/support/unit/helpers/active_record_versions.rb @@ -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 diff --git a/spec/support/unit/helpers/rails_versions.rb b/spec/support/unit/helpers/rails_versions.rb index b93767b9..9f4312f6 100644 --- a/spec/support/unit/helpers/rails_versions.rb +++ b/spec/support/unit/helpers/rails_versions.rb @@ -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? diff --git a/spec/unit/shoulda/matchers/active_model/allow_value_matcher_spec.rb b/spec/unit/shoulda/matchers/active_model/allow_value_matcher_spec.rb index 8a2dccfd..c2cee49c 100644 --- a/spec/unit/shoulda/matchers/active_model/allow_value_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_model/allow_value_matcher_spec.rb @@ -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 diff --git a/spec/unit/shoulda/matchers/active_model/disallow_value_matcher_spec.rb b/spec/unit/shoulda/matchers/active_model/disallow_value_matcher_spec.rb index 3b0539ad..74609a4f 100644 --- a/spec/unit/shoulda/matchers/active_model/disallow_value_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_model/disallow_value_matcher_spec.rb @@ -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 diff --git a/spec/unit/shoulda/matchers/active_model/exception_message_finder_spec.rb b/spec/unit/shoulda/matchers/active_model/exception_message_finder_spec.rb deleted file mode 100644 index 69438100..00000000 --- a/spec/unit/shoulda/matchers/active_model/exception_message_finder_spec.rb +++ /dev/null @@ -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 diff --git a/spec/unit/shoulda/matchers/active_model/validate_inclusion_of_matcher_spec.rb b/spec/unit/shoulda/matchers/active_model/validate_inclusion_of_matcher_spec.rb index 08a7edcf..9afc6935 100644 --- a/spec/unit/shoulda/matchers/active_model/validate_inclusion_of_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_model/validate_inclusion_of_matcher_spec.rb @@ -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], diff --git a/spec/unit/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb b/spec/unit/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb index f1b5863d..944142ca 100644 --- a/spec/unit/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb @@ -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 diff --git a/spec/unit/shoulda/matchers/active_model/validation_message_finder_spec.rb b/spec/unit/shoulda/matchers/active_model/validation_message_finder_spec.rb deleted file mode 100644 index 82ebdfa5..00000000 --- a/spec/unit/shoulda/matchers/active_model/validation_message_finder_spec.rb +++ /dev/null @@ -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 diff --git a/spec/unit_spec_helper.rb b/spec/unit_spec_helper.rb index 4ade91e5..5806eb3f 100644 --- a/spec/unit_spec_helper.rb +++ b/spec/unit_spec_helper.rb @@ -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