From 12e788f1d01be23a08237fe45eafe94313fa6214 Mon Sep 17 00:00:00 2001 From: Brian McManus Date: Wed, 28 Oct 2015 10:01:26 -0700 Subject: [PATCH] Numericality validation of virtual attributes Commit #18b2859d2522a4866c398b9a32ebc3de4ec79389 broke numericality validation of virtual attributes in ActiveRecord models. This commit added a `column_type` method that assumes if `columns_hash` is a thing then our attribute would be present within it. This is not true for virtual attributes and leads to: ``` NoMethodError: undefined method `type' for nil:NilClass # /Users/bdmac/.gem/ruby/2.2.2/gems/shoulda-matchers-3.0.1/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb:442:in # `column_type' ``` --- NEWS.md | 4 + .../validate_numericality_of_matcher.rb | 15 +++- .../validate_numericality_of_matcher_spec.rb | 84 +++++++++++++++++++ 3 files changed, 101 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 928cd7ce..7deefb05 100644 --- a/NEWS.md +++ b/NEWS.md @@ -17,6 +17,10 @@ get a CouldNotSetAttributeError again.* (You may get some more information if a test fails, however.) +* Fix `validate_numericality_of` so that it does not blow up when used against + a virtual attribute defined in an ActiveRecord model (that is, an attribute + that is not present in the database but is defined using `attr_accessor`). + ### Improvements * Improve failure messages and descriptions of all matchers across the board so diff --git a/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb b/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb index bf68f0e3..a6935ab4 100644 --- a/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +++ b/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb @@ -491,7 +491,8 @@ module Shoulda end def given_numeric_column? - [:integer, :float, :decimal].include?(column_type) + attribute_is_active_record_column? && + [:integer, :float, :decimal].include?(column_type) end private @@ -512,9 +513,19 @@ module Shoulda ) end + def attribute_is_active_record_column? + columns_hash.key?(@attribute.to_s) + end + def column_type + columns_hash[@attribute.to_s].type + end + + def columns_hash if @subject.class.respond_to?(:columns_hash) - @subject.class.columns_hash[@attribute.to_s].type + @subject.class.columns_hash + else + {} end end 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 b3b59e4a..0b4cd4fe 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 @@ -152,6 +152,13 @@ Example did not properly validate that :attr looks like a number. } ) + context 'when the attribute is a virtual attribute in an ActiveRecord model' do + it 'accepts' do + record = build_record_validating_numericality_of_virtual_attribute + expect(record).to validate_numericality + end + end + context 'when the column is an integer column' do it 'accepts (and does not raise an AttributeChangedValueError)' do record = build_record_validating_numericality(column_type: :integer) @@ -360,6 +367,15 @@ Example did not properly validate that :attr looks like an odd number. end end + context 'when the attribute is a virtual attribute in ActiveRecord model' do + it 'accepts' do + record = build_record_validating_numericality_of_virtual_attribute( + odd: true + ) + expect(record).to validate_numericality.odd + end + end + context 'when the column is an integer column' do it 'accepts (and does not raise an error)' do record = build_record_validating_numericality( @@ -453,6 +469,15 @@ Example did not properly validate that :attr looks like an even number. end end + context 'when the attribute is a virtual attribute in an ActiveRecord model' do + it 'accepts' do + record = build_record_validating_numericality_of_virtual_attribute( + even: true, + ) + expect(record).to validate_numericality.even + end + end + context 'when the column is an integer column' do it 'accepts (and does not raise an error)' do record = build_record_validating_numericality( @@ -551,6 +576,15 @@ than or equal to 18. end end + context 'when the attribute is a virtual attribute in an ActiveRecord model' do + it 'accepts' do + record = build_record_validating_numericality_of_virtual_attribute( + less_than_or_equal_to: 18, + ) + expect(record).to validate_numericality.is_less_than_or_equal_to(18) + end + end + context 'when the column is an integer column' do it 'accepts (and does not raise an error)' do record = build_record_validating_numericality( @@ -648,6 +682,15 @@ than 18. end end + context 'when the attribute is a virtual attribute in an ActiveRecord model' do + it 'accepts' do + record = build_record_validating_numericality_of_virtual_attribute( + less_than: 18, + ) + expect(record).to validate_numericality.is_less_than(18) + end + end + context 'when the column is an integer column' do it 'accepts (and does not raise an error)' do record = build_record_validating_numericality( @@ -743,6 +786,15 @@ to 18. end end + context 'when the attribute is a virtual attribute in an ActiveRecord model' do + it 'accepts' do + record = build_record_validating_numericality_of_virtual_attribute( + equal_to: 18, + ) + expect(record).to validate_numericality.is_equal_to(18) + end + end + context 'when the column is an integer column' do it 'accepts (and does not raise an error)' do record = build_record_validating_numericality( @@ -841,6 +893,16 @@ than or equal to 18. end end + context 'when the attribute is a virtual attribute in an ActiveRecord model' do + it 'accepts' do + record = build_record_validating_numericality_of_virtual_attribute( + greater_than_or_equal_to: 18, + ) + expect(record).to validate_numericality. + is_greater_than_or_equal_to(18) + end + end + context 'when the column is an integer column' do it 'accepts (and does not raise an error)' do record = build_record_validating_numericality( @@ -942,6 +1004,15 @@ than 18. end end + context 'when the attribute is a virtual attribute in an ActiveRecord model' do + it 'accepts' do + record = build_record_validating_numericality_of_virtual_attribute( + greater_than: 18, + ) + expect(record).to validate_numericality.is_greater_than(18) + end + end + context 'when the column is an integer column' do it 'accepts (and does not raise an error)' do record = build_record_validating_numericality( @@ -1715,6 +1786,19 @@ Example did not properly validate that :attr looks like a number. end end + def define_model_validating_numericality_of_virtual_attribute(options = {}) + attribute_name = options.delete(:attribute_name) { self.attribute_name } + + define_model 'Example' do |model| + model.send(:attr_accessor, attribute_name) + model.validates_numericality_of(attribute_name, options) + end + end + + def build_record_validating_numericality_of_virtual_attribute(options = {}) + define_model_validating_numericality_of_virtual_attribute(options).new + end + def build_record_validating_numericality(options = {}) define_model_validating_numericality(options).new end