module Remarkable
module ActiveRecord
module Matchers
class ValidateAssociatedMatcher < Remarkable::ActiveRecord::Base #:nodoc:
arguments :collection => :associations, :as => :association, :block => true
optional :message
optional :builder, :block => true
collection_assertions :find_association?, :is_valid?
default_options :message => :invalid
protected
def find_association?
reflection = @subject.class.reflect_on_association(@association)
raise ScriptError, "Could not find association #{@association} on #{subject_class}." unless reflection
associated_object = if builder = @options[:builder] || @block
builder.call(@subject)
elsif [:belongs_to, :has_one].include?(reflection.macro)
@subject.send(:"build_#{@association}") rescue nil
else
@subject.send(@association).build rescue nil
end
raise ScriptError, "The association object #{@association} could not be built. You can give me " <<
":builder as option or a block which returns an association." unless associated_object
raise ScriptError, "The associated object #{@association} is not invalid. You can give me " <<
":builder as option or a block which returns an invalid association." if associated_object.save
return true
end
def is_valid?
return false if @subject.valid?
error_message_to_expect = error_message_from_model(@subject, :base, @options[:message])
# In Rails 2.1.2, the error on association returns a symbol (:invalid)
# instead of the message, so we check this case here.
@subject.errors.on(@association) == @options[:message] ||
assert_contains(@subject.errors.on(@association), error_message_to_expect)
end
end
# Ensures that the model is invalid if one of the associations given is
# invalid. It tries to build the association automatically. In has_one
# and belongs_to cases, it will build it like this:
#
# @model.build_association
# @project.build_manager
#
# In has_many and has_and_belongs_to_many to cases it will build it like
# this:
#
# @model.association.build
# @project.tasks.build
#
# The object returned MUST be invalid and it's likely the case, since the
# associated object is empty when calling build. However, if the associated
# object has to be manipulated to be invalid, you will have to give :builder
# as option or a block to manipulate it:
#
# should_validate_associated(:tasks) do |project|
# project.tasks.build(:captcha => 'i_am_a_bot')
# end
#
# In the case above, the associated object task is only invalid when the
# captcha attribute is set. So we give a block to the matcher that tell
# exactly how to build an invalid object.
#
# The example above can also be written as:
#
# should_validate_associated :tasks, :builder => proc{ |p| p.tasks.build(:captcha => 'i_am_a_bot') }
#
# == Options
#
# * :builder - a proc to build the association
#
# * :message - value the test expects to find in errors.on(:attribute).
# Regexp, string or symbol. Default = I18n.translate('activerecord.errors.messages.invalid')
#
# == Examples
#
# should_validate_associated :tasks
# should_validate_associated :tasks, :builder => proc{ |p| p.tasks.build(:captcha => 'i_am_a_bot') }
#
# should_validate_associated :tasks do |m|
# m.builder { |p| p.tasks.build(:captcha => 'i_am_a_bot') }
# end
#
# it { should validate_associated(:tasks) }
# it { should validate_associated(:tasks, :builder => proc{ |p| p.tasks.build(:captcha => 'i_am_a_bot') }) }
#
def validate_associated(*args, &block)
ValidateAssociatedMatcher.new(*args, &block).spec(self)
end
end
end
end