diff --git a/NEWS.md b/NEWS.md index 031edf04..b8b9f96a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,8 @@ # HEAD +* Association matchers now test that the model being referred to (either + implicitly or explicitly, using `:class_name`) actually exists. + # v 2.5.0 * Fix Rails/Test::Unit integration to ensure that the test case classes we are diff --git a/lib/shoulda/matchers/active_record/association_matcher.rb b/lib/shoulda/matchers/active_record/association_matcher.rb index a04371fe..94ad6963 100644 --- a/lib/shoulda/matchers/active_record/association_matcher.rb +++ b/lib/shoulda/matchers/active_record/association_matcher.rb @@ -160,6 +160,7 @@ module Shoulda # :nodoc: @subject = subject association_exists? && macro_correct? && + class_exists? && foreign_key_exists? && class_name_correct? && conditions_correct? && @@ -257,6 +258,14 @@ module Shoulda # :nodoc: end end + def class_exists? + associated_class + true + rescue NameError + @missing = "#{reflection.class_name} does not exist" + false + end + def conditions_correct? if options.key?(:conditions) if option_verifier.correct_for_relation_clause?(:conditions, options[:conditions]) diff --git a/spec/shoulda/matchers/active_record/association_matcher_spec.rb b/spec/shoulda/matchers/active_record/association_matcher_spec.rb index c70cdaa3..fa105cc3 100644 --- a/spec/shoulda/matchers/active_record/association_matcher_spec.rb +++ b/spec/shoulda/matchers/active_record/association_matcher_spec.rb @@ -30,6 +30,7 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do end it 'accepts a polymorphic association' do + define_model :parent define_model :child, parent_type: :string, parent_id: :integer do belongs_to :parent, polymorphic: true end @@ -100,6 +101,21 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do belonging_to_parent.should_not belong_to(:parent).class_name('TreeChild') end + it 'rejects an association with non-existent implicit class name' do + belonging_to_non_existent_class(:child, :parent).should_not belong_to(:parent) + end + + it 'rejects an association with non-existent explicit class name' do + belonging_to_non_existent_class(:child, :parent, :class_name => 'Parent').should_not belong_to(:parent) + end + + it 'adds error message when rejecting an association with non-existent class' do + message = 'Expected Child to have a belongs_to association called parent (Parent2 does not exist)' + expect { + belonging_to_non_existent_class(:child, :parent, :class_name => 'Parent2').should belong_to(:parent) + }.to fail_with_message(message) + end + context 'an association with a :validate option' do [false, true].each do |validate_value| context "when the model has validate: #{validate_value}" do @@ -194,6 +210,12 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do belongs_to :parent, options end.new end + + def belonging_to_non_existent_class(model_name, assoc_name, options = {}) + define_model model_name, "#{assoc_name}_id" => :integer do + belongs_to assoc_name, options + end.new + end end context 'have_many' do @@ -343,6 +365,21 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do having_many_children.should_not have_many(:children).class_name('Node') end + it 'rejects an association with non-existent implicit class name' do + having_many_non_existent_class(:parent, :children).should_not have_many(:children) + end + + it 'rejects an association with non-existent explicit class name' do + having_many_non_existent_class(:parent, :children, :class_name => 'Child').should_not have_many(:children) + end + + it 'adds error message when rejecting an association with non-existent class' do + message = 'Expected Parent to have a has_many association called children (Child2 does not exist)' + expect { + having_many_non_existent_class(:parent, :children, :class_name => 'Child2').should have_many(:children) + }.to fail_with_message(message) + end + context 'validate' do it 'accepts when the :validate option matches' do having_many_children(validate: false).should have_many(:children).validate(false) @@ -396,6 +433,12 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do end end.new end + + def having_many_non_existent_class(model_name, assoc_name, options = {}) + define_model model_name do + has_many assoc_name, options + end.new + end end context 'have_one' do @@ -500,6 +543,21 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do having_one_detail.should_not have_one(:detail).class_name('NotSet') end + it 'rejects an association with non-existent implicit class name' do + having_one_non_existent(:pserson, :detail).should_not have_one(:detail) + end + + it 'rejects an association with non-existent explicit class name' do + having_one_non_existent(:person, :detail, :class_name => 'Detail').should_not have_one(:detail) + end + + it 'adds error message when rejecting an association with non-existent class' do + message = 'Expected Person to have a has_one association called detail (Detail2 does not exist)' + expect { + having_one_non_existent(:person, :detail, :class_name => 'Detail2').should have_one(:detail) + }.to fail_with_message(message) + end + it 'accepts an association with a through' do define_model :detail @@ -551,6 +609,12 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do end end.new end + + def having_one_non_existent(model_name, assoc_name, options = {}) + define_model model_name do + has_one assoc_name, options + end.new + end end context 'have_and_belong_to_many' do @@ -630,6 +694,24 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do should_not have_and_belong_to_many(:relatives).class_name('PersonRelatives') end + it 'rejects an association with non-existent implicit class name' do + having_and_belonging_to_many_non_existent_class(:person, :relatives). + should_not have_and_belong_to_many(:relatives) + end + + it 'rejects an association with non-existent explicit class name' do + having_and_belonging_to_many_non_existent_class(:person, :relatives, :class_name => 'Relative') + .should_not have_and_belong_to_many(:relatives) + end + + it 'adds error message when rejecting an association with non-existent class' do + message = 'Expected Person to have a has_and_belongs_to_many association called relatives (Relative2 does not exist)' + expect { + having_and_belonging_to_many_non_existent_class(:person, :relatives, :class_name => 'Relative2'). + should have_and_belong_to_many(:relatives) + }.to fail_with_message(message) + end + context 'validate' do it 'accepts when the :validate option matches' do having_and_belonging_to_many_relatives(validate: false). @@ -660,6 +742,12 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do has_and_belongs_to_many :relatives end.new end + + def having_and_belonging_to_many_non_existent_class(model_name, assoc_name, options = {}) + define_model model_name do + has_and_belongs_to_many assoc_name, options + end.new + end end def define_association_with_conditions(model, macro, name, conditions, other_options={})