module Shoulda # :nodoc: module Matchers module ActiveRecord # :nodoc: # Ensure that the belongs_to relationship exists. # # it { should belong_to(:parent) } # def belong_to(name) AssociationMatcher.new(:belongs_to, name) end # Ensures that the has_many relationship exists. Will also test that the # associated table has the required columns. Works with polymorphic # associations. # # Options: # * through - association name for has_many :through # * dependent - tests that the association makes use of the # dependent option. # * :class_name - tests that the association makes use of the class_name option. # # Example: # it { should have_many(:friends) } # it { should have_many(:enemies).through(:friends) } # it { should have_many(:enemies).dependent(:destroy) } # def have_many(name) AssociationMatcher.new(:has_many, name) end # Ensure that the has_one relationship exists. Will also test that the # associated table has the required columns. Works with polymorphic # associations. # # Options: # * :dependent - tests that the association makes use of the # dependent option. # * :class_name - tests that the association makes use of the class_name option. # # Example: # it { should have_one(:god) } # unless hindu # def have_one(name) AssociationMatcher.new(:has_one, name) end # Ensures that the has_and_belongs_to_many relationship exists, and that # the join table is in place. # # it { should have_and_belong_to_many(:posts) } # def have_and_belong_to_many(name) AssociationMatcher.new(:has_and_belongs_to_many, name) end class AssociationMatcher # :nodoc: def initialize(macro, name) @macro = macro @name = name end def through(through) @through = through self end def dependent(dependent) @dependent = dependent self end def order(order) @order = order self end def conditions(conditions) @conditions = conditions self end def class_name(class_name) @class_name = class_name self end def matches?(subject) @subject = subject association_exists? && macro_correct? && foreign_key_exists? && through_association_valid? && dependent_correct? && class_name_correct? && order_correct? && conditions_correct? && join_table_exists? end def failure_message "Expected #{expectation} (#{@missing})" end def negative_failure_message "Did not expect #{expectation}" end def description description = "#{macro_description} #{@name}" description += " through #{@through}" if @through description += " dependent => #{@dependent}" if @dependent description += " class_name => #{@class_name}" if @class_name description += " order => #{@order}" if @order description end protected def association_exists? if reflection.nil? @missing = "no association called #{@name}" false else true end end def macro_correct? if reflection.macro == @macro true else @missing = "actual association type was #{reflection.macro}" false end end def foreign_key_exists? !(belongs_foreign_key_missing? || has_foreign_key_missing?) end def belongs_foreign_key_missing? @macro == :belongs_to && !class_has_foreign_key?(model_class) end def has_foreign_key_missing? [:has_many, :has_one].include?(@macro) && !through? && !class_has_foreign_key?(associated_class) end def through_association_valid? @through.nil? || (through_association_exists? && through_association_correct?) end def through_association_exists? if through_reflection.nil? @missing = "#{model_class.name} does not have any relationship to #{@through}" false else true end end def through_association_correct? if @through == reflection.options[:through] true else @missing = "Expected #{model_class.name} to have #{@name} through #{@through}, " << "but got it through #{reflection.options[:through]}" false end end def dependent_correct? if @dependent.nil? || @dependent.to_s == reflection.options[:dependent].to_s true else @missing = "#{@name} should have #{@dependent} dependency" false end end def class_name_correct? if @class_name.nil? || @class_name.to_s == reflection.options[:class_name].to_s true else @missing = "#{@name} should have #{@class_name} as class_name" false end end def order_correct? if @order.nil? || @order.to_s == reflection.options[:order].to_s true else @missing = "#{@name} should be ordered by #{@order}" false end end def conditions_correct? if @conditions.nil? || @conditions.to_s == reflection.options[:conditions].to_s true else @missing = "#{@name} should have the following conditions: #{@conditions}" false end end def join_table_exists? if @macro != :has_and_belongs_to_many || ::ActiveRecord::Base.connection.tables.include?(join_table.to_s) true else @missing = "join table #{join_table} doesn't exist" false end end def class_has_foreign_key?(klass) if klass.column_names.include?(foreign_key.to_s) true else @missing = "#{klass} does not have a #{foreign_key} foreign key." false end end def model_class @subject.class end def join_table reflection.options[:join_table] end def associated_class reflection.klass end def foreign_key fk_reflection = reflection if [:has_one, :has_many].include?(@macro) && reflection.options.include?(:inverse_of) fk_reflection = associated_class.reflect_on_association( reflection.options[:inverse_of] ) end fk_reflection.respond_to?(:foreign_key) ? fk_reflection.foreign_key : fk_reflection.primary_key_name end def through? reflection.options[:through] end def reflection @reflection ||= model_class.reflect_on_association(@name) end def through_reflection @through_reflection ||= model_class.reflect_on_association(@through) end def expectation "#{model_class.name} to have a #{@macro} association called #{@name}" end def macro_description case @macro.to_s when 'belongs_to' then 'belong to' when 'has_many' then 'have many' when 'has_one' then 'have one' when 'has_and_belongs_to_many' then 'have and belong to many' end end end end end end