284 lines
7.9 KiB
Ruby
284 lines
7.9 KiB
Ruby
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:
|
|
# * <tt>through</tt> - association name for <tt>has_many :through</tt>
|
|
# * <tt>dependent</tt> - tests that the association makes use of the
|
|
# dependent option.
|
|
# * <tt>:class_name</tt> - 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:
|
|
# * <tt>:dependent</tt> - tests that the association makes use of the
|
|
# dependent option.
|
|
# * <tt>:class_name</tt> - 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
|