module Audited
module RspecMatchers
# Ensure that the model is audited.
#
# Options:
# * associated_with - tests that the audit makes use of the associated_with option
# * only - tests that the audit makes use of the only option *Overrides except option*
# * except - tests that the audit makes use of the except option
# * requires_comment - if specified, then the audit must require comments through the audit_comment attribute
# * on - tests that the audit makes use of the on option with specified parameters
#
# Example:
# it { should be_audited }
# it { should be_audited.associated_with(:user) }
# it { should be_audited.only(:field_name) }
# it { should be_audited.except(:password) }
# it { should be_audited.requires_comment }
# it { should be_audited.on(:create).associated_with(:user).except(:password) }
#
def be_audited
AuditMatcher.new
end
# Ensure that the model has associated audits
#
# Example:
# it { should have_associated_audits }
#
def have_associated_audits
AssociatedAuditMatcher.new
end
class AuditMatcher # :nodoc:
def initialize
@options = {}
end
def associated_with(model)
@options[:associated_with] = model
self
end
def only(*fields)
@options[:only] = fields.flatten
self
end
def except(*fields)
@options[:except] = fields.flatten
self
end
def requires_comment
@options[:comment_required] = true
self
end
def on(*actions)
@options[:on] = actions.flatten
self
end
def matches?(subject)
@subject = subject
auditing_enabled? &&
associated_with_model? &&
records_changes_to_specified_fields? &&
comment_required_valid?
end
def failure_message
"Expected #{@expectation}"
end
def negative_failure_message
"Did not expect #{@expectation}"
end
alias_method :failure_message_when_negated, :negative_failure_message
def description
description = "audited"
description += " associated with #{@options[:associated_with]}" if @options.key?(:associated_with)
description += " only => #{@options[:only].join ', '}" if @options.key?(:only)
description += " except => #{@options[:except].join(', ')}" if @options.key?(:except)
description += " requires audit_comment" if @options.key?(:comment_required)
description
end
protected
def expects(message)
@expectation = message
end
def auditing_enabled?
expects "#{model_class} to be audited"
model_class.respond_to?(:auditing_enabled) && model_class.auditing_enabled
end
def model_class
@subject.class
end
def associated_with_model?
expects "#{model_class} to record audits to associated model #{@options[:associated_with]}"
model_class.audit_associated_with == @options[:associated_with]
end
def records_changes_to_specified_fields?
if @options[:only] || @options[:except]
if @options[:only]
except = model_class.column_names - @options[:only].map(&:to_s)
else
except = model_class.default_ignored_attributes + Audited.ignored_attributes
except |= @options[:except].collect(&:to_s) if @options[:except]
end
expects "non audited columns (#{model_class.non_audited_columns.inspect}) to match (#{expect})"
model_class.non_audited_columns =~ except
else
true
end
end
def comment_required_valid?
if @options[:comment_required]
@subject.audit_comment = nil
expects "to be invalid when audit_comment is not specified"
@subject.valid? == false && @subject.errors.key?(:audit_comment)
else
true
end
end
end
class AssociatedAuditMatcher # :nodoc:
def matches?(subject)
@subject = subject
association_exists?
end
def failure_message
"Expected #{model_class} to have associated audits"
end
def negative_failure_message
"Expected #{model_class} to not have associated audits"
end
alias_method :failure_message_when_negated, :negative_failure_message
def description
"has associated audits"
end
protected
def model_class
@subject.class
end
def reflection
model_class.reflect_on_association(:associated_audits)
end
def association_exists?
!reflection.nil? &&
reflection.macro == :has_many &&
reflection.options[:class_name] == Audited.audit_class.name
end
end
end
end