require 'forwardable' module Shoulda # :nodoc: module Matchers module ActiveRecord # :nodoc: # Ensure that the belongs_to relationship exists. # # Options: # * :class_name - tests that the association resolves to class_name. # * :validate - tests that the association makes use of the validate # option. # * :touch - tests that the association makes use of the touch # option. # # Example: # 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 resoves to class_name. # * :validate - tests that the association makes use of the validate # 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 resolves to class_name. # * :validate - tests that the association makes use of the validate # 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. # # Options: # * :class_name - tests that the association resolves to class_name. # * :validate - tests that the association makes use of the validate # option. # # Example: # 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: delegate :reflection, :model_class, :associated_class, :through?, :join_table, to: :reflector def initialize(macro, name) @macro = macro @name = name @options = {} @submatchers = [] @missing = '' end def through(through) through_matcher = AssociationMatchers::ThroughMatcher.new(through, name) add_submatcher(through_matcher) self end def dependent(dependent) dependent_matcher = AssociationMatchers::DependentMatcher.new(dependent, name) add_submatcher(dependent_matcher) self end def order(order) order_matcher = AssociationMatchers::OrderMatcher.new(order, name) add_submatcher(order_matcher) self end def counter_cache(counter_cache = true) counter_cache_matcher = AssociationMatchers::CounterCacheMatcher.new(counter_cache, name) add_submatcher(counter_cache_matcher) self end def conditions(conditions) @options[:conditions] = conditions self end def class_name(class_name) @options[:class_name] = class_name self end def with_foreign_key(foreign_key) @options[:foreign_key] = foreign_key self end def validate(validate = true) @options[:validate] = validate self end def touch(touch = true) @options[:touch] = touch self end def description description = "#{macro_description} #{name}" description += " class_name => #{options[:class_name]}" if options.key?(:class_name) [description, submatchers.map(&:description)].flatten.join(' ') end def failure_message_for_should "Expected #{expectation} (#{missing_options})" end def failure_message_for_should_not "Did not expect #{expectation}" end def matches?(subject) @subject = subject association_exists? && macro_correct? && foreign_key_exists? && class_name_correct? && conditions_correct? && join_table_exists? && validate_correct? && touch_correct? && submatchers_match? end private attr_reader :submatchers, :missing, :subject, :macro, :name, :options def reflector @reflector ||= AssociationMatchers::ModelReflector.new(subject, name) end def option_verifier @option_verifier ||= AssociationMatchers::OptionVerifier.new(reflector) end def add_submatcher(matcher) @submatchers << matcher end def macro_description case macro.to_s when 'belongs_to' 'belong to' when 'has_many' 'have many' when 'has_one' 'have one' when 'has_and_belongs_to_many' 'have and belong to many' end end def expectation "#{model_class.name} to have a #{macro} association called #{name}" end def missing_options [missing, failing_submatchers.map(&:missing_option)].flatten.join end def failing_submatchers @failing_submatchers ||= submatchers.select do |matcher| !matcher.matches?(subject) end end 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 class_name_correct? if options.key?(:class_name) if option_verifier.correct_for_string?(:class_name, options[:class_name]) true else @missing = "#{name} should resolve to #{options[:class_name]} for class_name" false end else true end end def conditions_correct? if options.key?(:conditions) if option_verifier.correct_for_relation_clause?(:conditions, options[:conditions]) true else @missing = "#{name} should have the following conditions: #{options[:conditions]}" false end else true end end def join_table_exists? if macro != :has_and_belongs_to_many || model_class.connection.tables.include?(join_table) true else @missing = "join table #{join_table} doesn't exist" false end end def validate_correct? if option_verifier.correct_for_boolean?(:validate, options[:validate]) true else @missing = "#{name} should have :validate => #{options[:validate]}" false end end def touch_correct? if option_verifier.correct_for_boolean?(:touch, options[:touch]) true else @missing = "#{name} should have :touch => #{options[:touch]}" false end end def class_has_foreign_key?(klass) if options.key?(:foreign_key) option_verifier.correct_for_string?(:foreign_key, options[:foreign_key]) else if klass.column_names.include?(foreign_key) true else @missing = "#{klass} does not have a #{foreign_key} foreign key." false end end end def foreign_key if foreign_key_reflection if foreign_key_reflection.respond_to?(:foreign_key) foreign_key_reflection.foreign_key.to_s else foreign_key_reflection.primary_key_name.to_s end end end def foreign_key_reflection if [:has_one, :has_many].include?(macro) && reflection.options.include?(:inverse_of) associated_class.reflect_on_association(reflection.options[:inverse_of]) else reflection end end def submatchers_match? failing_submatchers.empty? end end end end end