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 (#{except})" 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