diff --git a/.gitignore b/.gitignore index 19412b49..c258e7e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,12 @@ +*.iml +*.ipr +*.iws *.log *.swp .bundle +.rakeTasks +.ruby-gemset +.ruby-version .rvmrc .yardoc coverage/ diff --git a/.travis.yml b/.travis.yml index 91b7331a..27ec5569 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,9 @@ +language: ruby +services: mongodb rvm: - - 1.8.7 - - 1.9.2 - 1.9.3 - - ruby-head -branches: - only: - - master - - v3 + - 2.0.0 + - 2.1.2 gemfile: - - gemfiles/rails30.gemfile - - gemfiles/rails31.gemfile - - gemfiles/rails32.gemfile + - gemfiles/rails40.gemfile + - gemfiles/rails41.gemfile diff --git a/Appraisals b/Appraisals index 5a83ad09..7aa64186 100644 --- a/Appraisals +++ b/Appraisals @@ -1,11 +1,9 @@ -appraise 'rails30' do - gem 'rails', '~> 3.0.0' +appraise 'rails40' do + gem 'rails', '~> 4.0.0' + gem 'rails-observers' end -appraise 'rails31' do - gem 'rails', '~> 3.1.0' -end - -appraise 'rails32' do - gem 'rails', '~> 3.2.0' +appraise 'rails41' do + gem 'rails', '~> 4.1.0' + gem 'rails-observers' end diff --git a/README.md b/README.md index 519b9b6d..ae76df9c 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,17 @@ -Audited [![Build Status](https://secure.travis-ci.org/collectiveidea/audited.png)](http://travis-ci.org/collectiveidea/audited) [![Dependency Status](https://gemnasium.com/collectiveidea/audited.png)](https://gemnasium.com/collectiveidea/audited) +Audited [![Build Status](https://secure.travis-ci.org/collectiveidea/audited.png)](http://travis-ci.org/collectiveidea/audited) [![Dependency Status](https://gemnasium.com/collectiveidea/audited.png)](https://gemnasium.com/collectiveidea/audited)[![Code Climate](https://codeclimate.com/github/collectiveidea/audited.png)](https://codeclimate.com/github/collectiveidea/audited) ======= -**Audited** (previously acts_as_audited) is an ORM extension that logs all changes to your models. Audited also allows you to record who made those changes, save comments and associate models related to the changes. Audited works with Rails 3. +**Audited** (previously acts_as_audited) is an ORM extension that logs all changes to your models. Audited also allows you to record who made those changes, save comments and associate models related to the changes. + +Audited currently (4.x) works with Rails 4. For Rails 3, use gem version 3.0 or see the [3.0-stable branch](https://github.com/collectiveidea/audited/tree/3.0-stable). ## Supported Rubies Audited supports and is [tested against](http://travis-ci.org/collectiveidea/audited) the following Ruby versions: -* 1.8.7 -* 1.9.2 * 1.9.3 -* Head +* 2.0.0 +* 2.1.2 Audited may work just fine with a Ruby version not listed above, but we can't guarantee that it will. If you'd like to maintain a Ruby that isn't listed, please let us know with a [pull request](https://github.com/collectiveidea/audited/pulls). @@ -30,7 +31,7 @@ The installation process depends on what ORM your app is using. Add the appropriate gem to your Gemfile: ```ruby -gem "audited-activerecord", "~> 3.0" +gem "audited-activerecord", "~> 4.0" ``` Then, from your Rails app directory, create the `audits` table: @@ -54,7 +55,7 @@ Upgrading will only make changes if changes are needed. ### MongoMapper ```ruby -gem "audited-mongo_mapper", "~> 3.0" +gem "audited-mongo_mapper", "~> 4.0" ``` ## Usage @@ -87,6 +88,40 @@ audit.action # => "update" audit.audited_changes # => {"name"=>["Steve", "Ryan"]} ``` +### Specifying columns + +By default, a new audit is created for any attribute changes. You can, however, limit the columns to be considered. + +```ruby +class User < ActiveRecord::Base + # All fields + # audited + + # Single field + # audited only: :name + + # Multiple fields + # audited only: [:name, :address] + + # All except certain fields + # audited except: :password +end +``` + +### Specifying callbacks + +By default, a new audit is created for any Create, Update or Destroy action. You can, however, limit the actions audited. + +```ruby +class User < ActiveRecord::Base + # All fields and actions + # audited + + # Single field, only audit Update and Destroy (not Create) + # audited only: :name, on: [:update, :destroy] +end +``` + ### Comments You can attach comments to each audit using an `audit_comment` attribute on your model. @@ -118,7 +153,7 @@ class PostsController < ApplicationController end ``` -To use a method other than `current_user`, put the following in an intializer: +To use a method other than `current_user`, put the following in an initializer: ```ruby Audited.current_user_method = :authenticated_user @@ -127,7 +162,7 @@ Audited.current_user_method = :authenticated_user Outside of a request, Audited can still record the user with the `as_user` method: ```ruby -Audit.as_user(User.find(1)) do +Audited.audit_class.as_user(User.find(1)) do post.update_attribute!(:title => "Hello, world!") end post.audits.last.user # => # @@ -172,13 +207,48 @@ user.audits.last.associated # => # company.associated_audits.last.auditable # => # ``` +### Disabling auditing + +If you want to disable auditing temporarily doing certain tasks, there are a few +methods available. + +To disable auditing on a save: + +```ruby +@user.save_without_auditing +``` + +or: + +```ruby +@user.without_auditing do + @user.save +end +``` + +To disable auditing on a column: + +```ruby +User.non_audited_columns = [:first_name, :last_name] +``` + +To disable auditing on an entire model: + +```ruby +User.auditing_enabled = false +``` + ## Gotchas -### Accessible Attributes +### Using attr_protected or strong_parameters -Audited assumes you are using `attr_accessible`, however, if you are using `attr_protected` or just going at it unprotected you will have to set the `:allow_mass_assignment => true` option. +Audited assumes you are using `attr_accessible`. If you're using +`attr_protected` or `strong_parameters`, you'll have to take an extra step or +two. -If using `attr_protected` be sure to add `audit_ids` to the list of protected attributes to prevent data loss. + +If you're using `strong_parameters` with Rails 3.x, be sure to add `:allow_mass_assignment => true` to your `audited` call; otherwise Audited will +interfere with `strong_parameters` and none of your `save` calls will work. ```ruby class User < ActiveRecord::Base @@ -186,6 +256,8 @@ class User < ActiveRecord::Base end ``` +If using `attr_protected`, add `:allow_mass_assignment => true`, and also be sure to add `audit_ids` to the list of protected attributes to prevent data loss. + ```ruby class User < ActiveRecord::Base audited :allow_mass_assignment => true diff --git a/Rakefile b/Rakefile old mode 100644 new mode 100755 index 0d4370a9..42bc8ff6 --- a/Rakefile +++ b/Rakefile @@ -2,6 +2,7 @@ require 'bundler/gem_helper' require 'rspec/core/rake_task' +require 'rake/testtask' require 'appraisal' Bundler::GemHelper.install_tasks(:name => 'audited') @@ -17,8 +18,16 @@ ADAPTERS.each do |adapter| end end -RSpec::Core::RakeTask.new(:spec => ADAPTERS) do |t| - t.pattern = 'spec/audited/*_spec.rb' +task :spec do + ADAPTERS.each do |adapter| + Rake::Task[adapter].invoke + end +end + +Rake::TestTask.new do |t| + t.libs << "test" + t.test_files = FileList['test/**/*_test.rb'] + t.verbose = true end -task :default => :spec +task :default => [:spec, :test] diff --git a/audited-activerecord.gemspec b/audited-activerecord.gemspec index 216e4669..089c94a3 100644 --- a/audited-activerecord.gemspec +++ b/audited-activerecord.gemspec @@ -1,19 +1,25 @@ # encoding: utf-8 +$:.push File.expand_path("../lib", __FILE__) +require "audited/version" +require "audited/active_record/version" + Gem::Specification.new do |gem| gem.name = 'audited-activerecord' - gem.version = '3.0.0.rc2' + gem.version = Audited::ActiveRecord::VERSION gem.authors = ['Brandon Keepers', 'Kenneth Kalmer', 'Daniel Morrison', 'Brian Ryckbost', 'Steve Richert', 'Ryan Glover'] gem.email = 'info@collectiveidea.com' gem.description = 'Log all changes to your ActiveRecord models' gem.summary = gem.description gem.homepage = 'https://github.com/collectiveidea/audited' + gem.license = 'MIT' - gem.add_dependency 'audited', gem.version - gem.add_dependency 'activerecord', '~> 3.0' + gem.add_dependency 'audited', Audited::VERSION + gem.add_dependency 'activerecord', '~> 4.0' gem.files = `git ls-files lib`.split($\).grep(/(active_?record|generators)/) + gem.files << 'LICENSE' gem.require_paths = ['lib'] end diff --git a/audited-mongo_mapper.gemspec b/audited-mongo_mapper.gemspec index 0deb038f..6405ba89 100644 --- a/audited-mongo_mapper.gemspec +++ b/audited-mongo_mapper.gemspec @@ -1,19 +1,25 @@ # encoding: utf-8 +$:.push File.expand_path("../lib", __FILE__) +require "audited/version" +require "audited/mongo_mapper/version" + Gem::Specification.new do |gem| gem.name = 'audited-mongo_mapper' - gem.version = '3.0.0.rc2' + gem.version = Audited::MongoMapper::VERSION gem.authors = ['Brandon Keepers', 'Kenneth Kalmer', 'Daniel Morrison', 'Brian Ryckbost', 'Steve Richert', 'Ryan Glover'] gem.email = 'info@collectiveidea.com' gem.description = 'Log all changes to your MongoMapper models' gem.summary = gem.description gem.homepage = 'https://github.com/collectiveidea/audited' + gem.license = 'MIT' - gem.add_dependency 'audited', gem.version - gem.add_dependency 'mongo_mapper', '~> 0.11' + gem.add_dependency 'audited', Audited::VERSION + gem.add_dependency 'mongo_mapper', '~> 0.12.0' gem.files = `git ls-files lib`.split($\).grep(/mongo_mapper/) + gem.files << 'LICENSE' gem.require_paths = ['lib'] end diff --git a/audited.gemspec b/audited.gemspec index 95384b22..3881179f 100644 --- a/audited.gemspec +++ b/audited.gemspec @@ -1,24 +1,28 @@ # encoding: utf-8 +$:.push File.expand_path("../lib", __FILE__) +require "audited/version" Gem::Specification.new do |gem| gem.name = 'audited' - gem.version = '3.0.0.rc2' + gem.version = Audited::VERSION gem.authors = ['Brandon Keepers', 'Kenneth Kalmer', 'Daniel Morrison', 'Brian Ryckbost', 'Steve Richert', 'Ryan Glover'] gem.email = 'info@collectiveidea.com' gem.description = 'Log all changes to your models' gem.summary = gem.description gem.homepage = 'https://github.com/collectiveidea/audited' + gem.license = 'MIT' - gem.add_development_dependency 'activerecord', '~> 3.0' - gem.add_development_dependency 'appraisal', '~> 0.4' + gem.add_dependency 'rails-observers', '~> 0.1.2' + gem.add_development_dependency "protected_attributes" + gem.add_development_dependency 'appraisal', '~> 1.0.0' gem.add_development_dependency 'bson_ext', '~> 1.6' - gem.add_development_dependency 'mongo_mapper', '~> 0.11' - gem.add_development_dependency 'rails', '~> 3.0' - gem.add_development_dependency 'rspec-rails', '~> 2.0' + gem.add_development_dependency 'mongo_mapper', '~> 0.13.0.beta2' + gem.add_development_dependency 'rails', '~> 4.0.0' + gem.add_development_dependency 'rspec-rails', '~> 3.0' gem.add_development_dependency 'sqlite3', '~> 1.0' - gem.files = `git ls-files`.split($\).reject{|f| f =~ /(lib\/audited\-|adapters|generators)/ } + gem.files = `git ls-files`.split($\).reject{|f| f =~ /(\.gemspec|lib\/audited\-|adapters|generators)/ } gem.test_files = gem.files.grep(/^spec\//) gem.require_paths = ['lib'] end diff --git a/gemfiles/rails30.gemfile b/gemfiles/rails30.gemfile deleted file mode 100644 index fcd19cc4..00000000 --- a/gemfiles/rails30.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rails", "~> 3.0.0" - -gemspec :name=>"audited", :path=>"../" \ No newline at end of file diff --git a/gemfiles/rails31.gemfile b/gemfiles/rails31.gemfile deleted file mode 100644 index b4ca15e2..00000000 --- a/gemfiles/rails31.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rails", "~> 3.1.0" - -gemspec :name=>"audited", :path=>"../" \ No newline at end of file diff --git a/gemfiles/rails32.gemfile b/gemfiles/rails32.gemfile deleted file mode 100644 index 483b5f90..00000000 --- a/gemfiles/rails32.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rails", "~> 3.2.0" - -gemspec :name=>"audited", :path=>"../" \ No newline at end of file diff --git a/gemfiles/rails40.gemfile b/gemfiles/rails40.gemfile new file mode 100644 index 00000000..47437a60 --- /dev/null +++ b/gemfiles/rails40.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rails", "~> 4.0.0" +gem "rails-observers" + +gemspec :name => "audited", :path => "../" diff --git a/gemfiles/rails41.gemfile b/gemfiles/rails41.gemfile new file mode 100644 index 00000000..9e9d0ee8 --- /dev/null +++ b/gemfiles/rails41.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rails", "~> 4.1.0" +gem "rails-observers" + +gemspec :name => "audited", :path => "../" diff --git a/lib/audited-rspec.rb b/lib/audited-rspec.rb new file mode 100644 index 00000000..c977fd9e --- /dev/null +++ b/lib/audited-rspec.rb @@ -0,0 +1,4 @@ +require 'audited/rspec_matchers' +module RSpec::Matchers + include Audited::RspecMatchers +end diff --git a/lib/audited.rb b/lib/audited.rb index 4c0274ce..5b8076e6 100644 --- a/lib/audited.rb +++ b/lib/audited.rb @@ -1,8 +1,15 @@ +require 'rails/observers/active_model/active_model' + + module Audited - VERSION = '3.0.0' + VERSION = '4.0.0' class << self attr_accessor :ignored_attributes, :current_user_method, :audit_class + + def store + Thread.current[:audited_store] ||= {} + end end @ignored_attributes = %w(lock_version created_at updated_at created_on updated_on) diff --git a/lib/audited/active_record/version.rb b/lib/audited/active_record/version.rb new file mode 100644 index 00000000..ee4caa25 --- /dev/null +++ b/lib/audited/active_record/version.rb @@ -0,0 +1,5 @@ +module Audited + module ActiveRecord + VERSION = "4.0.0" + end +end \ No newline at end of file diff --git a/lib/audited/adapters/active_record/audit.rb b/lib/audited/adapters/active_record/audit.rb index aafce7d5..d711a372 100644 --- a/lib/audited/adapters/active_record/audit.rb +++ b/lib/audited/adapters/active_record/audit.rb @@ -11,24 +11,26 @@ module ActiveRecord # * action: one of create, update, or delete # * audited_changes: a serialized hash of all the changes # * comment: a comment set with the audit + # * version: the version of the model + # * request_uuid: a uuid based that allows audits from the same controller request # * created_at: Time that the change was performed # class Audit < ::ActiveRecord::Base include Audited::Audit - + include ActiveModel::Observing serialize :audited_changes - default_scope order(:version) - scope :descending, reorder("version DESC") - scope :creates, :conditions => {:action => 'create'} - scope :updates, :conditions => {:action => 'update'} - scope :destroys, :conditions => {:action => 'destroy'} - - scope :up_until, lambda {|date_or_time| where("created_at <= ?", date_or_time) } - scope :from_version, lambda {|version| where(['version >= ?', version]) } - scope :to_version, lambda {|version| where(['version <= ?', version]) } + default_scope ->{ order(:version)} + scope :descending, ->{ reorder("version DESC")} + scope :creates, ->{ where({:action => 'create'})} + scope :updates, ->{ where({:action => 'update'})} + scope :destroys, ->{ where({:action => 'destroy'})} + scope :up_until, ->(date_or_time){where("created_at <= ?", date_or_time) } + scope :from_version, ->(version){where(['version >= ?', version]) } + scope :to_version, ->(version){where(['version <= ?', version]) } + scope :auditable_finder, ->(auditable_id, auditable_type){where(auditable_id: auditable_id, auditable_type: auditable_type)} # Return all audits older than the current one. def ancestors self.class.where(['auditable_id = ? and auditable_type = ? and version <= ?', @@ -56,11 +58,7 @@ def user_as_string private def set_version_number - max = self.class.maximum(:version, - :conditions => { - :auditable_id => auditable_id, - :auditable_type => auditable_type - }) || 0 + max = self.class.auditable_finder(auditable_id, auditable_type).maximum(:version) || 0 self.version = max + 1 end end diff --git a/lib/audited/adapters/mongo_mapper/audit.rb b/lib/audited/adapters/mongo_mapper/audit.rb index 325434a5..a03fabb6 100644 --- a/lib/audited/adapters/mongo_mapper/audit.rb +++ b/lib/audited/adapters/mongo_mapper/audit.rb @@ -29,6 +29,7 @@ class Audit key :version, Integer, :default => 0 key :comment, String key :remote_address, String + key :request_uuid, String key :created_at, Time include Audited::Audit diff --git a/lib/audited/audit.rb b/lib/audited/audit.rb index 7d9586fb..f0bb0b55 100644 --- a/lib/audited/audit.rb +++ b/lib/audited/audit.rb @@ -11,12 +11,10 @@ def setup_audit belongs_to :user, :polymorphic => true belongs_to :associated, :polymorphic => true - before_create :set_version_number, :set_audit_user + before_create :set_version_number, :set_audit_user, :set_request_uuid cattr_accessor :audited_class_names self.audited_class_names = Set.new - - attr_accessible :action, :audited_changes, :comment, :associated end # Returns the list of classes that are being audited @@ -29,12 +27,9 @@ def audited_classes # for background operations that require audit information. def as_user(user, &block) Thread.current[:audited_user] = user - - yieldval = yield - + yield + ensure Thread.current[:audited_user] = nil - - yieldval end # @private @@ -101,5 +96,9 @@ def set_audit_user self.user = Thread.current[:audited_user] if Thread.current[:audited_user] nil # prevent stopping callback chains end + + def set_request_uuid + self.request_uuid ||= SecureRandom.uuid + end end end diff --git a/lib/audited/auditor.rb b/lib/audited/auditor.rb index 44f39e8d..2ab1821d 100644 --- a/lib/audited/auditor.rb +++ b/lib/audited/auditor.rb @@ -30,19 +30,10 @@ module ClassMethods # class User < ActiveRecord::Base # audited :except => :password # end - # * +protect+ - If your model uses +attr_protected+, set this to false to prevent Rails from - # raising an error. If you declare +attr_accessible+ before calling +audited+, it - # will automatically default to false. You only need to explicitly set this if you are - # calling +attr_accessible+ after. # # * +require_comment+ - Ensures that audit_comment is supplied before # any create, update or destroy operation. # - # class User < ActiveRecord::Base - # audited :protect => false - # attr_accessible :name - # end - # def audited(options = {}) # don't allow multiple calls return if self.included_modules.include?(Audited::Auditor::AuditedInstanceMethods) @@ -66,9 +57,6 @@ def audited(options = {}) end attr_accessor :audit_comment - unless options[:allow_mass_assignment] - attr_accessible :audit_comment - end has_many :audits, :as => :auditable, :class_name => Audited.audit_class.name Audited.audit_class.audited_class_names << self.to_s @@ -77,10 +65,12 @@ def audited(options = {}) before_update :audit_update if !options[:on] || (options[:on] && options[:on].include?(:update)) before_destroy :audit_destroy if !options[:on] || (options[:on] && options[:on].include?(:destroy)) - # Define and set an after_audit callback. This might be useful if you want - # to notify a party after the audit has been created. + # Define and set after_audit and around_audit callbacks. This might be useful if you want + # to notify a party after the audit has been created or if you want to access the newly-created + # audit. define_callbacks :audit set_callback :audit, :after, :after_audit, :if => lambda { self.respond_to?(:after_audit) } + set_callback :audit, :around, :around_audit, :if => lambda { self.respond_to?(:around_audit) } attr_accessor :version diff --git a/lib/audited/mongo_mapper/version.rb b/lib/audited/mongo_mapper/version.rb new file mode 100644 index 00000000..de25a572 --- /dev/null +++ b/lib/audited/mongo_mapper/version.rb @@ -0,0 +1,5 @@ +module Audited + module MongoMapper + VERSION = "4.0.0" + end +end \ No newline at end of file diff --git a/lib/audited/rspec_matchers.rb b/lib/audited/rspec_matchers.rb new file mode 100644 index 00000000..701ec451 --- /dev/null +++ b/lib/audited/rspec_matchers.rb @@ -0,0 +1,173 @@ +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 + + 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 + + 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 diff --git a/lib/audited/sweeper.rb b/lib/audited/sweeper.rb index 26943dac..28815dce 100644 --- a/lib/audited/sweeper.rb +++ b/lib/audited/sweeper.rb @@ -1,5 +1,8 @@ +require "rails/observers/activerecord/active_record" +require "rails/observers/action_controller/caching" + module Audited - class Sweeper < ActiveModel::Observer + class Sweeper < ActionController::Caching::Sweeper observe Audited.audit_class attr_accessor :controller @@ -15,13 +18,18 @@ def around(controller) def before_create(audit) audit.user ||= current_user - audit.remote_address = controller.try(:request).try(:ip) + audit.remote_address = controller.try(:request).try(:remote_ip) + audit.request_uuid = request_uuid if request_uuid end def current_user controller.send(Audited.current_user_method) if controller.respond_to?(Audited.current_user_method, true) end + def request_uuid + controller.try(:request).try(:uuid) + end + def add_observer!(klass) super define_callback(klass) @@ -35,11 +43,28 @@ def define_callback(klass) end klass.send(:before_create, callback_meth) end + + def controller + ::Audited.store[:current_controller] + end + + def controller=(value) + ::Audited.store[:current_controller] = value + end end end if defined?(ActionController) and defined?(ActionController::Base) + # Create dynamic subclass of Audited::Sweeper otherwise rspec will + # fail with both ActiveRecord and MongoMapper tests as there will be + # around_filter collision + sweeper_class = Class.new(Audited::Sweeper) do + def self.name + "#{Audited.audit_class}::Sweeper" + end + end + ActionController::Base.class_eval do - around_filter Audited::Sweeper.instance + around_filter sweeper_class.instance end end diff --git a/lib/audited/version.rb b/lib/audited/version.rb new file mode 100644 index 00000000..ce9d3a2f --- /dev/null +++ b/lib/audited/version.rb @@ -0,0 +1,3 @@ +module Audited + VERSION = "4.0.0" +end \ No newline at end of file diff --git a/lib/generators/audited/install_generator.rb b/lib/generators/audited/install_generator.rb index a48a0f3a..925d5e70 100644 --- a/lib/generators/audited/install_generator.rb +++ b/lib/generators/audited/install_generator.rb @@ -13,7 +13,7 @@ class InstallGenerator < Rails::Generators::Base # Implement the required interface for Rails::Generators::Migration. def self.next_migration_number(dirname) #:nodoc: next_migration_number = current_migration_number(dirname) + 1 - if ActiveRecord::Base.timestamped_migrations + if ::ActiveRecord::Base.timestamped_migrations [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max else "%.3d" % next_migration_number diff --git a/lib/generators/audited/templates/add_request_uuid_to_audits.rb b/lib/generators/audited/templates/add_request_uuid_to_audits.rb new file mode 100644 index 00000000..a58a4832 --- /dev/null +++ b/lib/generators/audited/templates/add_request_uuid_to_audits.rb @@ -0,0 +1,10 @@ +class <%= migration_class_name %> < ActiveRecord::Migration + def self.up + add_column :audits, :request_uuid, :string + add_index :audits, :request_uuid + end + + def self.down + remove_column :audits, :request_uuid + end +end diff --git a/lib/generators/audited/templates/install.rb b/lib/generators/audited/templates/install.rb index 0dbb478c..ab351823 100644 --- a/lib/generators/audited/templates/install.rb +++ b/lib/generators/audited/templates/install.rb @@ -13,12 +13,14 @@ def self.up t.column :version, :integer, :default => 0 t.column :comment, :string t.column :remote_address, :string + t.column :request_uuid, :string t.column :created_at, :datetime end add_index :audits, [:auditable_id, :auditable_type], :name => 'auditable_index' add_index :audits, [:associated_id, :associated_type], :name => 'associated_index' add_index :audits, [:user_id, :user_type], :name => 'user_index' + add_index :audits, :request_uuid add_index :audits, :created_at end diff --git a/lib/generators/audited/upgrade_generator.rb b/lib/generators/audited/upgrade_generator.rb index 16cc4a83..4343fa30 100644 --- a/lib/generators/audited/upgrade_generator.rb +++ b/lib/generators/audited/upgrade_generator.rb @@ -13,7 +13,7 @@ class UpgradeGenerator < Rails::Generators::Base # Implement the required interface for Rails::Generators::Migration. def self.next_migration_number(dirname) #:nodoc: next_migration_number = current_migration_number(dirname) + 1 - if ActiveRecord::Base.timestamped_migrations + if ::ActiveRecord::Base.timestamped_migrations [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max else "%.3d" % next_migration_number @@ -44,6 +44,10 @@ def migrations_to_be_applied yield :add_remote_address_to_audits end + unless columns.include?( 'request_uuid' ) + yield :add_request_uuid_to_audits + end + unless columns.include?( 'association_id' ) if columns.include?('auditable_parent_id') yield :rename_parent_to_association diff --git a/spec/audited/adapters/active_record/active_record_spec_helper.rb b/spec/audited/adapters/active_record/active_record_spec_helper.rb index f401545a..d26e2cb6 100644 --- a/spec/audited/adapters/active_record/active_record_spec_helper.rb +++ b/spec/audited/adapters/active_record/active_record_spec_helper.rb @@ -2,3 +2,4 @@ require 'support/active_record/schema' require 'audited/adapters/active_record' require 'support/active_record/models' +load "audited/sweeper.rb" # force to reload sweeper diff --git a/spec/audited/adapters/active_record/audit_spec.rb b/spec/audited/adapters/active_record/audit_spec.rb index 090f87bd..60bea96c 100644 --- a/spec/audited/adapters/active_record/audit_spec.rb +++ b/spec/audited/adapters/active_record/audit_spec.rb @@ -7,7 +7,7 @@ it "should be able to set the user to a model object" do subject.user = user - subject.user.should == user + expect(subject.user).to eq(user) end it "should be able to set the user to nil" do @@ -17,28 +17,28 @@ subject.user = nil - subject.user.should be_nil - subject.user_id.should be_nil - subject.user_type.should be_nil - subject.username.should be_nil + expect(subject.user).to be_nil + expect(subject.user_id).to be_nil + expect(subject.user_type).to be_nil + expect(subject.username).to be_nil end it "should be able to set the user to a string" do subject.user = 'test' - subject.user.should == 'test' + expect(subject.user).to eq('test') end it "should clear model when setting to a string" do subject.user = user subject.user = 'testing' - subject.user_id.should be_nil - subject.user_type.should be_nil + expect(subject.user_id).to be_nil + expect(subject.user_type).to be_nil end it "should clear the username when setting to a model" do subject.username = 'test' subject.user = user - subject.username.should be_nil + expect(subject.username).to be_nil end end @@ -50,7 +50,7 @@ 5.times { |i| user.update_attribute :name, (i + 2).to_s } user.audits.each do |audit| - audit.revision.name.should == audit.version.to_s + expect(audit.revision.name).to eq(audit.version.to_s) end end @@ -59,41 +59,46 @@ u.update_attribute :logins, 1 u.update_attribute :logins, 2 - u.audits[2].revision.logins.should be(2) - u.audits[1].revision.logins.should be(1) - u.audits[0].revision.logins.should be(0) + expect(u.audits[2].revision.logins).to eq(2) + expect(u.audits[1].revision.logins).to eq(1) + expect(u.audits[0].revision.logins).to eq(0) end it "should bypass attribute assignment wrappers" do u = Models::ActiveRecord::User.create(:name => '') - u.audits.first.revision.name.should == '<Joe>' + expect(u.audits.first.revision.name).to eq('<Joe>') end it "should work for deleted records" do user = Models::ActiveRecord::User.create :name => "1" user.destroy revision = user.audits.last.revision - revision.name.should == user.name - revision.should be_a_new_record + expect(revision.name).to eq(user.name) + expect(revision).to be_a_new_record end end it "should set the version number on create" do user = Models::ActiveRecord::User.create! :name => 'Set Version Number' - user.audits.first.version.should be(1) + expect(user.audits.first.version).to eq(1) user.update_attribute :name, "Set to 2" - user.audits(true).first.version.should be(1) - user.audits(true).last.version.should be(2) + expect(user.audits(true).first.version).to eq(1) + expect(user.audits(true).last.version).to eq(2) user.destroy - Audited.audit_class.where(:auditable_type => 'Models::ActiveRecord::User', :auditable_id => user.id).last.version.should be(3) + expect(Audited.audit_class.where(:auditable_type => 'Models::ActiveRecord::User', :auditable_id => user.id).last.version).to eq(3) + end + + it "should set the request uuid on create" do + user = Models::ActiveRecord::User.create! :name => 'Set Request UUID' + expect(user.audits(true).first.request_uuid).not_to be_blank end describe "reconstruct_attributes" do it "should work with the old way of storing just the new value" do audits = Audited.audit_class.reconstruct_attributes([Audited.audit_class.new(:audited_changes => {'attribute' => 'value'})]) - audits['attribute'].should == 'value' + expect(audits['attribute']).to eq('value') end end @@ -106,18 +111,19 @@ class Models::ActiveRecord::CustomUserSubclass < Models::ActiveRecord::CustomUse end it "should include audited classes" do - Audited.audit_class.audited_classes.should include(Models::ActiveRecord::User) + expect(Audited.audit_class.audited_classes).to include(Models::ActiveRecord::User) end it "should include subclasses" do - Audited.audit_class.audited_classes.should include(Models::ActiveRecord::CustomUserSubclass) + expect(Audited.audit_class.audited_classes).to include(Models::ActiveRecord::CustomUserSubclass) end end describe "new_attributes" do it "should return a hash of the new values" do - Audited.audit_class.new(:audited_changes => {:a => [1, 2], :b => [3, 4]}).new_attributes.should == {'a' => 2, 'b' => 4} + new_attributes = Audited.audit_class.new(:audited_changes => {:a => [1, 2], :b => [3, 4]}).new_attributes + expect(new_attributes).to eq({'a' => 2, 'b' => 4}) end end @@ -125,7 +131,8 @@ class Models::ActiveRecord::CustomUserSubclass < Models::ActiveRecord::CustomUse describe "old_attributes" do it "should return a hash of the old values" do - Audited.audit_class.new(:audited_changes => {:a => [1, 2], :b => [3, 4]}).old_attributes.should == {'a' => 1, 'b' => 3} + old_attributes = Audited.audit_class.new(:audited_changes => {:a => [1, 2], :b => [3, 4]}).old_attributes + expect(old_attributes).to eq({'a' => 1, 'b' => 3}) end end @@ -139,7 +146,7 @@ class Models::ActiveRecord::CustomUserSubclass < Models::ActiveRecord::CustomUse company.save company.audits.each do |audit| - audit.user.should == user + expect(audit.user).to eq(user) end end end @@ -151,7 +158,7 @@ class Models::ActiveRecord::CustomUserSubclass < Models::ActiveRecord::CustomUse company.save company.audits.each do |audit| - audit.username.should == user.name + expect(audit.username).to eq(user.name) end end end @@ -161,13 +168,13 @@ class Models::ActiveRecord::CustomUserSubclass < Models::ActiveRecord::CustomUse t1 = Thread.new do Audited.audit_class.as_user(user) do sleep 1 - Models::ActiveRecord::Company.create(:name => 'The Auditors, Inc').audits.first.user.should == user + expect(Models::ActiveRecord::Company.create(:name => 'The Auditors, Inc').audits.first.user).to eq(user) end end t2 = Thread.new do Audited.audit_class.as_user(user.name) do - Models::ActiveRecord::Company.create(:name => 'The Competing Auditors, LLC').audits.first.username.should == user.name + expect(Models::ActiveRecord::Company.create(:name => 'The Competing Auditors, LLC').audits.first.username).to eq(user.name) sleep 0.5 end end @@ -180,17 +187,21 @@ class Models::ActiveRecord::CustomUserSubclass < Models::ActiveRecord::CustomUse end it "should return the value from the yield block" do - Audited.audit_class.as_user('foo') do + result = Audited.audit_class.as_user('foo') do 42 - end.should == 42 + end + expect(result).to eq(42) end - end - - describe "mass assignment" do - it "should accept :action, :audited_changes and :comment attributes as well as the :associated association" do - Audited.audit_class.accessible_attributes.should include(:action, :audited_changes, :comment, :associated) + it "should reset audited_user when the yield block raises an exception" do + expect { + Audited.audit_class.as_user('foo') do + raise StandardError + end + }.to raise_exception + expect(Thread.current[:audited_user]).to be_nil end + end end diff --git a/spec/audited/adapters/active_record/auditor_spec.rb b/spec/audited/adapters/active_record/auditor_spec.rb index 804b9126..788315d5 100644 --- a/spec/audited/adapters/active_record/auditor_spec.rb +++ b/spec/audited/adapters/active_record/auditor_spec.rb @@ -4,16 +4,16 @@ describe "configuration" do it "should include instance methods" do - Models::ActiveRecord::User.new.should be_a_kind_of( Audited::Auditor::AuditedInstanceMethods) + expect(Models::ActiveRecord::User.new).to be_a_kind_of( Audited::Auditor::AuditedInstanceMethods) end it "should include class methods" do - Models::ActiveRecord::User.should be_a_kind_of( Audited::Auditor::AuditedClassMethods ) + expect(Models::ActiveRecord::User).to be_a_kind_of( Audited::Auditor::AuditedClassMethods ) end ['created_at', 'updated_at', 'created_on', 'updated_on', 'lock_version', 'id', 'password'].each do |column| it "should not audit #{column}" do - Models::ActiveRecord::User.non_audited_columns.should include(column) + expect(Models::ActiveRecord::User.non_audited_columns).to include(column) end end @@ -23,11 +23,11 @@ class Secret < ::ActiveRecord::Base audited end - Secret.non_audited_columns.should include('delta', 'top_secret', 'created_at') + expect(Secret.non_audited_columns).to include('delta', 'top_secret', 'created_at') end it "should not save non-audited columns" do - create_active_record_user.audits.first.audited_changes.keys.any? { |col| ['created_at', 'updated_at', 'password'].include?( col ) }.should be_false + expect(create_active_record_user.audits.first.audited_changes.keys.any? { |col| ['created_at', 'updated_at', 'password'].include?( col ) }).to eq(false) end end @@ -42,12 +42,12 @@ class Secret < ::ActiveRecord::Base :suspended_at => yesterday, :logins => 2) - u.name.should eq('name') - u.username.should eq('username') - u.password.should eq('password') - u.activated.should eq(true) - u.suspended_at.should eq(yesterday) - u.logins.should eq(2) + expect(u.name).to eq('name') + expect(u.username).to eq('username') + expect(u.password).to eq('password') + expect(u.activated).to eq(true) + expect(u.suspended_at).to eq(yesterday) + expect(u.logins).to eq(2) end end @@ -61,28 +61,28 @@ class Secret < ::ActiveRecord::Base end it "should create associated audit" do - user.audits.count.should be(1) + expect(user.audits.count).to eq(1) end it "should set the action to create" do - user.audits.first.action.should == 'create' - Audited.audit_class.creates.reorder(:id).last.should == user.audits.first - user.audits.creates.count.should == 1 - user.audits.updates.count.should == 0 - user.audits.destroys.count.should == 0 + expect(user.audits.first.action).to eq('create') + expect(Audited.audit_class.creates.reorder(:id).last).to eq(user.audits.first) + expect(user.audits.creates.count).to eq(1) + expect(user.audits.updates.count).to eq(0) + expect(user.audits.destroys.count).to eq(0) end it "should store all the audited attributes" do - user.audits.first.audited_changes.should == user.audited_attributes + expect(user.audits.first.audited_changes).to eq(user.audited_attributes) end it "should store comment" do - user.audits.first.comment.should == 'Create' + expect(user.audits.first.comment).to eq('Create') end it "should not audit an attribute which is excepted if specified on create or destroy" do on_create_destroy_except_name = Models::ActiveRecord::OnCreateDestroyExceptName.create(:name => 'Bart') - on_create_destroy_except_name.audits.first.audited_changes.keys.any?{|col| ['name'].include? col}.should be_false + expect(on_create_destroy_except_name.audits.first.audited_changes.keys.any?{|col| ['name'].include? col}).to eq(false) end it "should not save an audit if only specified on update/destroy" do @@ -108,18 +108,18 @@ class Secret < ::ActiveRecord::Base it "should set the action to 'update'" do @user.update_attributes :name => 'Changed' - @user.audits.last.action.should == 'update' - Audited.audit_class.updates.reorder(:id).last.should == @user.audits.last - @user.audits.updates.last.should == @user.audits.last + expect(@user.audits.last.action).to eq('update') + expect(Audited.audit_class.updates.reorder(:id).last).to eq(@user.audits.last) + expect(@user.audits.updates.last).to eq(@user.audits.last) end it "should store the changed attributes" do @user.update_attributes :name => 'Changed' - @user.audits.last.audited_changes.should == { 'name' => ['Brandon', 'Changed'] } + expect(@user.audits.last.audited_changes).to eq({ 'name' => ['Brandon', 'Changed'] }) end it "should store audit comment" do - @user.audits.last.comment.should == 'Update' + expect(@user.audits.last.comment).to eq('Update') end it "should not save an audit if only specified on create/destroy" do @@ -162,21 +162,21 @@ class Secret < ::ActiveRecord::Base @user.destroy }.to change( Audited.audit_class, :count ) - @user.audits.size.should be(2) + expect(@user.audits.size).to eq(2) end it "should set the action to 'destroy'" do @user.destroy - @user.audits.last.action.should == 'destroy' - Audited.audit_class.destroys.reorder(:id).last.should == @user.audits.last - @user.audits.destroys.last.should == @user.audits.last + expect(@user.audits.last.action).to eq('destroy') + expect(Audited.audit_class.destroys.reorder(:id).last).to eq(@user.audits.last) + expect(@user.audits.destroys.last).to eq(@user.audits.last) end it "should store all of the audited attributes" do @user.destroy - @user.audits.last.audited_changes.should == @user.audited_attributes + expect(@user.audits.last.audited_changes).to eq(@user.audited_attributes) end it "should be able to reconstruct a destroyed record without history" do @@ -184,7 +184,7 @@ class Secret < ::ActiveRecord::Base @user.destroy revision = @user.audits.first.revision - revision.name.should == @user.name + expect(revision.name).to eq(@user.name) end it "should not save an audit if only specified on create/update" do @@ -194,6 +194,17 @@ class Secret < ::ActiveRecord::Base on_create_update.destroy }.to_not change( Audited.audit_class, :count ) end + + it "should audit dependent destructions" do + owner = Models::ActiveRecord::Owner.create! + company = owner.companies.create! + + expect { + owner.destroy + }.to change( Audited.audit_class, :count ) + + expect(company.audits.map { |a| a.action }).to eq(['create', 'destroy']) + end end describe "associated with" do @@ -201,17 +212,17 @@ class Secret < ::ActiveRecord::Base let(:owned_company) { Models::ActiveRecord::OwnedCompany.create!(:name => 'The auditors', :owner => owner) } it "should record the associated object on create" do - owned_company.audits.first.associated.should == owner + expect(owned_company.audits.first.associated).to eq(owner) end it "should store the associated object on update" do owned_company.update_attribute(:name, 'The Auditors') - owned_company.audits.last.associated.should == owner + expect(owned_company.audits.last.associated).to eq(owner) end it "should store the associated object on destroy" do owned_company.destroy - owned_company.audits.last.associated.should == owner + expect(owned_company.audits.last.associated).to eq(owner) end end @@ -220,8 +231,8 @@ class Secret < ::ActiveRecord::Base let!(:owned_company) { Models::ActiveRecord::OwnedCompany.create!(:name => 'The auditors', :owner => owner) } it "should list the associated audits" do - owner.associated_audits.length.should == 1 - owner.associated_audits.first.auditable.should == owned_company + expect(owner.associated_audits.length).to eq(1) + expect(owner.associated_audits.first.auditable).to eq(owned_company) end end @@ -229,16 +240,16 @@ class Secret < ::ActiveRecord::Base let( :user ) { create_versions } it "should return an Array of Users" do - user.revisions.should be_a_kind_of( Array ) - user.revisions.each { |version| version.should be_a_kind_of Models::ActiveRecord::User } + expect(user.revisions).to be_a_kind_of( Array ) + user.revisions.each { |version| expect(version).to be_a_kind_of Models::ActiveRecord::User } end it "should have one revision for a new record" do - create_active_record_user.revisions.size.should be(1) + expect(create_active_record_user.revisions.size).to eq(1) end it "should have one revision for each audit" do - user.audits.size.should eql( user.revisions.size ) + expect(user.audits.size).to eql( user.revisions.size ) end it "should set the attributes for each revision" do @@ -246,16 +257,16 @@ class Secret < ::ActiveRecord::Base u.update_attributes :name => 'Foobar' u.update_attributes :name => 'Awesome', :username => 'keepers' - u.revisions.size.should == 3 + expect(u.revisions.size).to eql(3) - u.revisions[0].name.should == 'Brandon' - u.revisions[0].username.should == 'brandon' + expect(u.revisions[0].name).to eql('Brandon') + expect(u.revisions[0].username).to eql('brandon') - u.revisions[1].name.should == 'Foobar' - u.revisions[1].username.should == 'brandon' + expect(u.revisions[1].name).to eql('Foobar') + expect(u.revisions[1].username).to eql('brandon') - u.revisions[2].name.should == 'Awesome' - u.revisions[2].username.should == 'keepers' + expect(u.revisions[2].name).to eql('Awesome') + expect(u.revisions[2].username).to eql('keepers') end it "access to only recent revisions" do @@ -263,18 +274,18 @@ class Secret < ::ActiveRecord::Base u.update_attributes :name => 'Foobar' u.update_attributes :name => 'Awesome', :username => 'keepers' - u.revisions(2).size.should == 2 + expect(u.revisions(2).size).to eq(2) - u.revisions(2)[0].name.should == 'Foobar' - u.revisions(2)[0].username.should == 'brandon' + expect(u.revisions(2)[0].name).to eq('Foobar') + expect(u.revisions(2)[0].username).to eq('brandon') - u.revisions(2)[1].name.should == 'Awesome' - u.revisions(2)[1].username.should == 'keepers' + expect(u.revisions(2)[1].name).to eq('Awesome') + expect(u.revisions(2)[1].username).to eq('keepers') end it "should be empty if no audits exist" do user.audits.delete_all - user.revisions.should be_empty + expect(user.revisions).to be_empty end it "should ignore attributes that have been deleted" do @@ -287,27 +298,27 @@ class Secret < ::ActiveRecord::Base let( :user ) { create_versions(5) } it "should maintain identity" do - user.revision(1).should == user + expect(user.revision(1)).to eq(user) end it "should find the given revision" do revision = user.revision(3) - revision.should be_a_kind_of( Models::ActiveRecord::User ) - revision.version.should be(3) - revision.name.should == 'Foobar 3' + expect(revision).to be_a_kind_of( Models::ActiveRecord::User ) + expect(revision.version).to eq(3) + expect(revision.name).to eq('Foobar 3') end it "should find the previous revision with :previous" do revision = user.revision(:previous) - revision.version.should be(4) - #revision.should == user.revision(4) - revision.attributes.should == user.revision(4).attributes + expect(revision.version).to eq(4) + #expect(revision).to eq(user.revision(4)) + expect(revision.attributes).to eq(user.revision(4).attributes) end it "should be able to get the previous revision repeatedly" do previous = user.revision(:previous) - previous.version.should be(4) - previous.revision(:previous).version.should be(3) + expect(previous.version).to eq(4) + expect(previous.revision(:previous).version).to eq(3) end it "should be able to set protected attributes" do @@ -315,14 +326,14 @@ class Secret < ::ActiveRecord::Base u.update_attribute :logins, 1 u.update_attribute :logins, 2 - u.revision(3).logins.should be(2) - u.revision(2).logins.should be(1) - u.revision(1).logins.should be(0) + expect(u.revision(3).logins).to eq(2) + expect(u.revision(2).logins).to eq(1) + expect(u.revision(1).logins).to eq(0) end it "should set attributes directly" do u = Models::ActiveRecord::User.create(:name => '') - u.revision(1).name.should == '<Joe>' + expect(u.revision(1).name).to eq('<Joe>') end it "should set the attributes for each revision" do @@ -330,20 +341,20 @@ class Secret < ::ActiveRecord::Base u.update_attributes :name => 'Foobar' u.update_attributes :name => 'Awesome', :username => 'keepers' - u.revision(3).name.should == 'Awesome' - u.revision(3).username.should == 'keepers' + expect(u.revision(3).name).to eq('Awesome') + expect(u.revision(3).username).to eq('keepers') - u.revision(2).name.should == 'Foobar' - u.revision(2).username.should == 'brandon' + expect(u.revision(2).name).to eq('Foobar') + expect(u.revision(2).username).to eq('brandon') - u.revision(1).name.should == 'Brandon' - u.revision(1).username.should == 'brandon' + expect(u.revision(1).name).to eq('Brandon') + expect(u.revision(1).username).to eq('brandon') end it "should be able to get time for first revision" do - suspended_at = Time.now + suspended_at = Time.zone.now u = Models::ActiveRecord::User.create(:suspended_at => suspended_at) - u.revision(1).suspended_at.should == suspended_at + expect(u.revision(1).suspended_at.to_s).to eq(suspended_at.to_s) end it "should not raise an error when no previous audits exist" do @@ -352,7 +363,7 @@ class Secret < ::ActiveRecord::Base end it "should mark revision's attributes as changed" do - user.revision(1).name_changed?.should be_true + expect(user.revision(1).name_changed?).to eq(true) end it "should record new audit when saving revision" do @@ -377,11 +388,11 @@ class Secret < ::ActiveRecord::Base audit.created_at = 1.hour.ago audit.save! user.update_attributes :name => 'updated' - user.revision_at( 2.minutes.ago ).version.should be(1) + expect(user.revision_at( 2.minutes.ago ).version).to eq(1) end it "should be nil if given a time before audits" do - user.revision_at( 1.week.ago ).should be_nil + expect(user.revision_at( 1.week.ago )).to be_nil end end @@ -389,7 +400,7 @@ class Secret < ::ActiveRecord::Base it "should not save an audit when calling #save_without_auditing" do expect { u = Models::ActiveRecord::User.new(:name => 'Brandon') - u.save_without_auditing.should be_true + expect(u.save_without_auditing).to eq(true) }.to_not change( Audited.audit_class, :count ) end @@ -404,16 +415,16 @@ class Secret < ::ActiveRecord::Base describe "on create" do it "should not validate when audit_comment is not supplied" do - Models::ActiveRecord::CommentRequiredUser.new.should_not be_valid + expect(Models::ActiveRecord::CommentRequiredUser.new).not_to be_valid end it "should validate when audit_comment is supplied" do - Models::ActiveRecord::CommentRequiredUser.new( :audit_comment => 'Create').should be_valid + expect(Models::ActiveRecord::CommentRequiredUser.new( :audit_comment => 'Create')).to be_valid end it "should validate when audit_comment is not supplied, and auditing is disabled" do Models::ActiveRecord::CommentRequiredUser.disable_auditing - Models::ActiveRecord::CommentRequiredUser.new.should be_valid + expect(Models::ActiveRecord::CommentRequiredUser.new).to be_valid Models::ActiveRecord::CommentRequiredUser.enable_auditing end end @@ -422,16 +433,16 @@ class Secret < ::ActiveRecord::Base let( :user ) { Models::ActiveRecord::CommentRequiredUser.create!( :audit_comment => 'Create' ) } it "should not validate when audit_comment is not supplied" do - user.update_attributes(:name => 'Test').should be_false + expect(user.update_attributes(:name => 'Test')).to eq(false) end it "should validate when audit_comment is supplied" do - user.update_attributes(:name => 'Test', :audit_comment => 'Update').should be_true + expect(user.update_attributes(:name => 'Test', :audit_comment => 'Update')).to eq(true) end it "should validate when audit_comment is not supplied, and auditing is disabled" do Models::ActiveRecord::CommentRequiredUser.disable_auditing - user.update_attributes(:name => 'Test').should be_true + expect(user.update_attributes(:name => 'Test')).to eq(true) Models::ActiveRecord::CommentRequiredUser.enable_auditing end end @@ -440,17 +451,17 @@ class Secret < ::ActiveRecord::Base let( :user ) { Models::ActiveRecord::CommentRequiredUser.create!( :audit_comment => 'Create' )} it "should not validate when audit_comment is not supplied" do - user.destroy.should be_false + expect(user.destroy).to eq(false) end it "should validate when audit_comment is supplied" do user.audit_comment = "Destroy" - user.destroy.should == user + expect(user.destroy).to eq(user) end it "should validate when audit_comment is not supplied, and auditing is disabled" do Models::ActiveRecord::CommentRequiredUser.disable_auditing - user.destroy.should == user + expect(user.destroy).to eq(user) Models::ActiveRecord::CommentRequiredUser.enable_auditing end end @@ -481,7 +492,7 @@ class Secret < ::ActiveRecord::Base company.update_attributes :name => 'The Auditors' company.audits.each do |audit| - audit.user.should == user + expect(audit.user).to eq(user) end end end @@ -492,7 +503,7 @@ class Secret < ::ActiveRecord::Base company.update_attributes :name => 'The Auditors' company.audits.each do |audit| - audit.user.should == user.name + expect(audit.user).to eq(user.name) end end end @@ -502,10 +513,19 @@ class Secret < ::ActiveRecord::Base let( :user ) { user = Models::ActiveRecord::UserWithAfterAudit.new } it "should invoke after_audit callback on create" do - user.bogus_attr.should == nil - user.save.should == true - user.bogus_attr.should == "do something" + expect(user.bogus_attr).to be_nil + expect(user.save).to eq(true) + expect(user.bogus_attr).to eq("do something") end end + describe "around_audit" do + let( :user ) { user = Models::ActiveRecord::UserWithAfterAudit.new } + + it "should invoke around_audit callback on create" do + expect(user.around_attr).to be_nil + expect(user.save).to eq(true) + expect(user.around_attr).to eq(user.audits.last) + end + end end diff --git a/spec/audited/adapters/active_record/sweeper_spec.rb b/spec/audited/adapters/active_record/sweeper_spec.rb index dca27819..3a056497 100644 --- a/spec/audited/adapters/active_record/sweeper_spec.rb +++ b/spec/audited/adapters/active_record/sweeper_spec.rb @@ -19,6 +19,7 @@ def update_user describe AuditsController, :adapter => :active_record do include RSpec::Rails::ControllerExampleGroup + render_views before(:each) do Audited.current_user_method = :current_user @@ -30,12 +31,11 @@ def update_user it "should audit user" do controller.send(:current_user=, user) - expect { post :audit }.to change( Audited.audit_class, :count ) - assigns(:company).audits.last.user.should == user + expect(assigns(:company).audits.last.user).to eq(user) end it "should support custom users for sweepers" do @@ -46,7 +46,7 @@ def update_user post :audit }.to change( Audited.audit_class, :count ) - assigns(:company).audits.last.user.should == user + expect(assigns(:company).audits.last.user).to eq(user) end it "should record the remote address responsible for the change" do @@ -55,7 +55,16 @@ def update_user post :audit - assigns(:company).audits.last.remote_address.should == '1.2.3.4' + expect(assigns(:company).audits.last.remote_address).to eq('1.2.3.4') + end + + it "should record a UUID for the web request responsible for the change" do + allow_any_instance_of(ActionDispatch::Request).to receive(:uuid).and_return("abc123") + controller.send(:current_user=, user) + + post :audit + + expect(assigns(:company).audits.last.request_uuid).to eq("abc123") end end @@ -72,3 +81,26 @@ def update_user end end + + +describe Audited::Sweeper, :adapter => :active_record do + + it "should be thread-safe" do + t1 = Thread.new do + sleep 0.5 + Audited::Sweeper.instance.controller = 'thread1 controller instance' + expect(Audited::Sweeper.instance.controller).to eq('thread1 controller instance') + end + + t2 = Thread.new do + Audited::Sweeper.instance.controller = 'thread2 controller instance' + sleep 1 + expect(Audited::Sweeper.instance.controller).to eq('thread2 controller instance') + end + + t1.join; t2.join + + expect(Audited::Sweeper.instance.controller).to be_nil + end + +end diff --git a/spec/audited/adapters/mongo_mapper/audit_spec.rb b/spec/audited/adapters/mongo_mapper/audit_spec.rb index 6dcb4dff..5f5eead2 100644 --- a/spec/audited/adapters/mongo_mapper/audit_spec.rb +++ b/spec/audited/adapters/mongo_mapper/audit_spec.rb @@ -5,14 +5,14 @@ it "sets created_at timestamp when audit is created" do subject.save! - subject.created_at.should be_a Time + expect(subject.created_at).to be_a Time end describe "user=" do it "should be able to set the user to a model object" do subject.user = user - subject.user.should == user + expect(subject.user).to eq(user) end it "should be able to set the user to nil" do @@ -22,28 +22,28 @@ subject.user = nil - subject.user.should be_nil - subject.user_id.should be_nil - subject.user_type.should be_nil - subject.username.should be_nil + expect(subject.user).to be_nil + expect(subject.user_id).to be_nil + expect(subject.user_type).to be_nil + expect(subject.username).to be_nil end it "should be able to set the user to a string" do subject.user = 'test' - subject.user.should == 'test' + expect(subject.user).to eq('test') end it "should clear model when setting to a string" do subject.user = user subject.user = 'testing' - subject.user_id.should be_nil - subject.user_type.should be_nil + expect(subject.user_id).to be_nil + expect(subject.user_type).to be_nil end it "should clear the username when setting to a model" do subject.username = 'test' subject.user = user - subject.username.should be_nil + expect(subject.username).to be_nil end end @@ -55,7 +55,7 @@ 5.times { |i| user.update_attribute :name, (i + 2).to_s } user.audits.each do |audit| - audit.revision.name.should == audit.version.to_s + expect(audit.revision.name).to eq(audit.version.to_s) end end @@ -64,42 +64,48 @@ u.update_attribute :logins, 1 u.update_attribute :logins, 2 - u.audits[2].revision.logins.should be(2) - u.audits[1].revision.logins.should be(1) - u.audits[0].revision.logins.should be(0) + expect(u.audits[2].revision.logins).to eq(2) + expect(u.audits[1].revision.logins).to eq(1) + expect(u.audits[0].revision.logins).to eq(0) end it "should bypass attribute assignment wrappers" do u = Models::MongoMapper::User.create(:name => '') - u.audits.first.revision.name.should == '<Joe>' + expect(u.audits.first.revision.name).to eq('<Joe>') end it "should work for deleted records" do user = Models::MongoMapper::User.create :name => "1" user.destroy revision = user.audits.last.revision - revision.name.should == user.name - revision.should be_a_new_record + expect(revision.name).to eq(user.name) + expect(revision).to be_a_new_record end end it "should set the version number on create" do user = Models::MongoMapper::User.create! :name => 'Set Version Number' - user.audits.first.version.should be(1) + expect(user.audits.first.version).to eq(1) user.update_attribute :name, "Set to 2" audits = user.audits.reload.all - audits.first.version.should be(1) - audits.last.version.should be(2) + expect(audits.first.version).to eq(1) + expect(audits.last.version).to eq(2) user.destroy - Audited.audit_class.where(:auditable_type => 'Models::MongoMapper::User', :auditable_id => user.id).all.last.version.should be(3) + expect(Audited.audit_class.where(:auditable_type => 'Models::MongoMapper::User', :auditable_id => user.id).all.last.version).to eq(3) + end + + it "should set the request uuid on create" do + user = Models::MongoMapper::User.create! :name => 'Set Request UUID' + audits = user.audits.reload.all + expect(audits.first.request_uuid).not_to be_blank end describe "reconstruct_attributes" do it "should work with the old way of storing just the new value" do audits = Audited.audit_class.reconstruct_attributes([Audited.audit_class.new(:audited_changes => {'attribute' => 'value'})]) - audits['attribute'].should == 'value' + expect(audits['attribute']).to eq('value') end end @@ -113,18 +119,19 @@ class Models::MongoMapper::CustomUserSubclass < Models::MongoMapper::CustomUser end it "should include audited classes" do - Audited.audit_class.audited_classes.should include(Models::MongoMapper::User) + expect(Audited.audit_class.audited_classes).to include(Models::MongoMapper::User) end it "should include subclasses" do - Audited.audit_class.audited_classes.should include(Models::MongoMapper::CustomUserSubclass) + expect(Audited.audit_class.audited_classes).to include(Models::MongoMapper::CustomUserSubclass) end end describe "new_attributes" do it "should return a hash of the new values" do - Audited.audit_class.new(:audited_changes => {:a => [1, 2], :b => [3, 4]}).new_attributes.should == {'a' => 2, 'b' => 4} + new_attributes = Audited.audit_class.new(:audited_changes => {:a => [1, 2], :b => [3, 4]}).new_attributes + expect(new_attributes).to eq({'a' => 2, 'b' => 4}) end end @@ -132,7 +139,8 @@ class Models::MongoMapper::CustomUserSubclass < Models::MongoMapper::CustomUser describe "old_attributes" do it "should return a hash of the old values" do - Audited.audit_class.new(:audited_changes => {:a => [1, 2], :b => [3, 4]}).old_attributes.should == {'a' => 1, 'b' => 3} + old_attributes = Audited.audit_class.new(:audited_changes => {:a => [1, 2], :b => [3, 4]}).old_attributes + expect(old_attributes).to eq({'a' => 1, 'b' => 3}) end end @@ -146,7 +154,7 @@ class Models::MongoMapper::CustomUserSubclass < Models::MongoMapper::CustomUser company.save company.audits.each do |audit| - audit.user.should == user + expect(audit.user).to eq(user) end end end @@ -158,7 +166,7 @@ class Models::MongoMapper::CustomUserSubclass < Models::MongoMapper::CustomUser company.save company.audits.each do |audit| - audit.username.should == user.name + expect(audit.username).to eq(user.name) end end end @@ -167,13 +175,13 @@ class Models::MongoMapper::CustomUserSubclass < Models::MongoMapper::CustomUser t1 = Thread.new do Audited.audit_class.as_user(user) do sleep 1 - Models::MongoMapper::Company.create(:name => 'The Auditors, Inc').audits.first.user.should == user + expect(Models::MongoMapper::Company.create(:name => 'The Auditors, Inc').audits.first.user).to eq(user) end end t2 = Thread.new do Audited.audit_class.as_user(user.name) do - Models::MongoMapper::Company.create(:name => 'The Competing Auditors, LLC').audits.first.username.should == user.name + expect(Models::MongoMapper::Company.create(:name => 'The Competing Auditors, LLC').audits.first.username).to eq(user.name) sleep 0.5 end end @@ -183,9 +191,19 @@ class Models::MongoMapper::CustomUserSubclass < Models::MongoMapper::CustomUser end it "should return the value from the yield block" do - Audited.audit_class.as_user('foo') do + result = Audited.audit_class.as_user('foo') do 42 - end.should == 42 + end + expect(result).to eq(42) + end + + it "should reset audited_user when the yield block raises an exception" do + expect { + Audited.audit_class.as_user('foo') do + raise StandardError + end + }.to raise_exception + expect(Thread.current[:audited_user]).to be_nil end end diff --git a/spec/audited/adapters/mongo_mapper/auditor_spec.rb b/spec/audited/adapters/mongo_mapper/auditor_spec.rb index 0007934b..28571a2d 100644 --- a/spec/audited/adapters/mongo_mapper/auditor_spec.rb +++ b/spec/audited/adapters/mongo_mapper/auditor_spec.rb @@ -4,16 +4,16 @@ describe "configuration" do it "should include instance methods" do - Models::MongoMapper::User.new.should be_a_kind_of(Audited::Auditor::AuditedInstanceMethods) + expect(Models::MongoMapper::User.new).to be_a_kind_of(Audited::Auditor::AuditedInstanceMethods) end it "should include class methods" do - Models::MongoMapper::User.should be_a_kind_of( Audited::Auditor::AuditedClassMethods ) + expect(Models::MongoMapper::User).to be_a_kind_of( Audited::Auditor::AuditedClassMethods ) end ['created_at', 'updated_at', 'created_on', 'updated_on', 'lock_version', 'id', '_id', 'password'].each do |column| it "should not audit #{column}" do - Models::MongoMapper::User.non_audited_columns.should include(column) + expect(Models::MongoMapper::User.non_audited_columns).to include(column) end end @@ -24,11 +24,11 @@ class Secret audited end - Secret.non_audited_columns.should include('delta', 'top_secret', 'created_at') + expect(Secret.non_audited_columns).to include('delta', 'top_secret', 'created_at') end it "should not save non-audited columns" do - create_mongo_user.audits.first.audited_changes.keys.any? { |col| ['created_at', 'updated_at', 'password'].include?( col ) }.should be_false + expect(create_mongo_user.audits.first.audited_changes.keys.any? { |col| ['created_at', 'updated_at', 'password'].include?( col ) }).to eq(false) end end @@ -43,12 +43,12 @@ class Secret :suspended_at => yesterday, :logins => 2) - u.name.should eq('name') - u.username.should eq('username') - u.password.should eq('password') - u.activated.should eq(true) - u.suspended_at.to_i.should eq(yesterday.to_i) - u.logins.should eq(2) + expect(u.name).to eq('name') + expect(u.username).to eq('username') + expect(u.password).to eq('password') + expect(u.activated).to eq(true) + expect(u.suspended_at.to_i).to eq(yesterday.to_i) + expect(u.logins).to eq(2) end end @@ -62,28 +62,28 @@ class Secret end it "should create associated audit" do - user.audits.count.should be(1) + expect(user.audits.count).to eq(1) end it "should set the action to create" do - user.audits.first.action.should == 'create' - Audited.audit_class.creates.sort(:id.asc).last.should == user.audits.first - user.audits.creates.count.should == 1 - user.audits.updates.count.should == 0 - user.audits.destroys.count.should == 0 + expect(user.audits.first.action).to eq('create') + expect(Audited.audit_class.creates.sort(:id.asc).last).to eq(user.audits.first) + expect(user.audits.creates.count).to eq(1) + expect(user.audits.updates.count).to eq(0) + expect(user.audits.destroys.count).to eq(0) end it "should store all the audited attributes" do - user.audits.first.audited_changes.should == user.audited_attributes + expect(user.audits.first.audited_changes).to eq(user.audited_attributes) end it "should store comment" do - user.audits.first.comment.should == 'Create' + expect(user.audits.first.comment).to eq('Create') end it "should not audit an attribute which is excepted if specified on create or destroy" do on_create_destroy_except_name = Models::MongoMapper::OnCreateDestroyExceptName.create(:name => 'Bart') - on_create_destroy_except_name.audits.first.audited_changes.keys.any?{|col| ['name'].include? col}.should be_false + expect(on_create_destroy_except_name.audits.first.audited_changes.keys.any?{|col| ['name'].include? col}).to eq(false) end it "should not save an audit if only specified on update/destroy" do @@ -109,19 +109,19 @@ class Secret it "should set the action to 'update'" do @user.update_attributes :name => 'Changed' - @user.audits.all.last.action.should == 'update' - Audited.audit_class.updates.sort(:id.asc).last.should == @user.audits.all.last - @user.audits.updates.last.should == @user.audits.all.last + expect(@user.audits.all.last.action).to eq('update') + expect(Audited.audit_class.updates.sort(:id.asc).last).to eq(@user.audits.all.last) + expect(@user.audits.updates.last).to eq(@user.audits.all.last) end it "should store the changed attributes" do now = Time.at(Time.now.to_i).utc @user.update_attributes :name => 'Changed', :suspended_at => now - @user.audits.all.last.audited_changes.should == { 'name' => ['Brandon', 'Changed'], 'suspended_at' => [nil, now] } + expect(@user.audits.all.last.audited_changes).to eq({ 'name' => ['Brandon', 'Changed'], 'suspended_at' => [nil, now] }) end it "should store audit comment" do - @user.audits.all.last.comment.should == 'Update' + expect(@user.audits.all.last.comment).to eq('Update') end it "should not save an audit if only specified on create/destroy" do @@ -143,12 +143,12 @@ class Secret expect { user.update_attribute(:name, 'O.J. Simpson') - }.to_not raise_error(BSON::InvalidDocument) + }.to_not raise_error change = user.audits.all.last.audited_changes['name'] - change.should be_all{|c| c.is_a?(String) } - change[0].should == 'Bart Simpson' - change[1].should == 'O.J. Simpson' + expect(change).to be_all{|c| c.is_a?(String) } + expect(change[0]).to eq('Bart Simpson') + expect(change[1]).to eq('O.J. Simpson') end describe "with no dirty changes" do @@ -177,21 +177,21 @@ class Secret @user.destroy }.to change( Audited.audit_class, :count ) - @user.audits.size.should be(2) + expect(@user.audits.size).to eq(2) end it "should set the action to 'destroy'" do @user.destroy - @user.audits.all.last.action.should == 'destroy' - Audited.audit_class.destroys.sort(:id.asc).last.should == @user.audits.all.last - @user.audits.destroys.last.should == @user.audits.all.last + expect(@user.audits.all.last.action).to eq('destroy') + expect(Audited.audit_class.destroys.sort(:id.asc).last).to eq(@user.audits.all.last) + expect(@user.audits.destroys.last).to eq(@user.audits.all.last) end it "should store all of the audited attributes" do @user.destroy - @user.audits.all.last.audited_changes.should == @user.audited_attributes + expect(@user.audits.all.last.audited_changes).to eq(@user.audited_attributes) end it "should be able to reconstruct a destroyed record without history" do @@ -199,7 +199,7 @@ class Secret @user.destroy revision = @user.audits.first.revision - revision.name.should == @user.name + expect(revision.name).to eq(@user.name) end it "should not save an audit if only specified on create/update" do @@ -216,17 +216,17 @@ class Secret let(:owned_company) { Models::MongoMapper::OwnedCompany.create!(:name => 'The auditors', :owner => owner) } it "should record the associated object on create" do - owned_company.audits.first.associated.should == owner + expect(owned_company.audits.first.associated).to eq(owner) end it "should store the associated object on update" do owned_company.update_attribute(:name, 'The Auditors') - owned_company.audits.all.last.associated.should == owner + expect(owned_company.audits.all.last.associated).to eq(owner) end it "should store the associated object on destroy" do owned_company.destroy - owned_company.audits.all.last.associated.should == owner + expect(owned_company.audits.all.last.associated).to eq(owner) end end @@ -235,8 +235,8 @@ class Secret let!(:owned_company) { Models::MongoMapper::OwnedCompany.create!(:name => 'The auditors', :owner => owner) } it "should list the associated audits" do - owner.associated_audits.length.should == 1 - owner.associated_audits.first.auditable.should == owned_company + expect(owner.associated_audits.length).to eq(1) + expect(owner.associated_audits.first.auditable).to eq(owned_company) end end @@ -244,16 +244,16 @@ class Secret let( :user ) { create_mongo_versions } it "should return an Array of Users" do - user.revisions.should be_a_kind_of( Array ) - user.revisions.each { |version| version.should be_a_kind_of Models::MongoMapper::User } + expect(user.revisions).to be_a_kind_of( Array ) + user.revisions.each { |version| expect(version).to be_a_kind_of(Models::MongoMapper::User) } end it "should have one revision for a new record" do - create_mongo_user.revisions.size.should be(1) + expect(create_mongo_user.revisions.size).to eq(1) end it "should have one revision for each audit" do - user.audits.size.should eql( user.revisions.size ) + expect(user.audits.size).to eql( user.revisions.size ) end it "should set the attributes for each revision" do @@ -261,16 +261,16 @@ class Secret u.update_attributes :name => 'Foobar' u.update_attributes :name => 'Awesome', :username => 'keepers' - u.revisions.size.should == 3 + expect(u.revisions.size).to eq(3) - u.revisions[0].name.should == 'Brandon' - u.revisions[0].username.should == 'brandon' + expect(u.revisions[0].name).to eq('Brandon') + expect(u.revisions[0].username).to eq('brandon') - u.revisions[1].name.should == 'Foobar' - u.revisions[1].username.should == 'brandon' + expect(u.revisions[1].name).to eq('Foobar') + expect(u.revisions[1].username).to eq('brandon') - u.revisions[2].name.should == 'Awesome' - u.revisions[2].username.should == 'keepers' + expect(u.revisions[2].name).to eq('Awesome') + expect(u.revisions[2].username).to eq('keepers') end it "access to only recent revisions" do @@ -278,18 +278,18 @@ class Secret u.update_attributes :name => 'Foobar' u.update_attributes :name => 'Awesome', :username => 'keepers' - u.revisions(2).size.should == 2 + expect(u.revisions(2).size).to eq(2) - u.revisions(2)[0].name.should == 'Foobar' - u.revisions(2)[0].username.should == 'brandon' + expect(u.revisions(2)[0].name).to eq('Foobar') + expect(u.revisions(2)[0].username).to eq('brandon') - u.revisions(2)[1].name.should == 'Awesome' - u.revisions(2)[1].username.should == 'keepers' + expect(u.revisions(2)[1].name).to eq('Awesome') + expect(u.revisions(2)[1].username).to eq('keepers') end it "should be empty if no audits exist" do user.audits.delete_all - user.revisions.should be_empty + expect(user.revisions).to be_empty end it "should ignore attributes that have been deleted" do @@ -302,27 +302,27 @@ class Secret let( :user ) { create_mongo_versions(5) } it "should maintain identity" do - user.revision(1).should == user + expect(user.revision(1)).to eq(user) end it "should find the given revision" do revision = user.revision(3) - revision.should be_a_kind_of( Models::MongoMapper::User ) - revision.version.should be(3) - revision.name.should == 'Foobar 3' + expect(revision).to be_a_kind_of( Models::MongoMapper::User ) + expect(revision.version).to eq(3) + expect(revision.name).to eq('Foobar 3') end it "should find the previous revision with :previous" do revision = user.revision(:previous) - revision.version.should be(4) - #revision.should == user.revision(4) - revision.attributes.should == user.revision(4).attributes + expect(revision.version).to eq(4) + #expect(revision).to eq(user.revision(4)) + expect(revision.attributes).to eq(user.revision(4).attributes) end it "should be able to get the previous revision repeatedly" do previous = user.revision(:previous) - previous.version.should be(4) - previous.revision(:previous).version.should be(3) + expect(previous.version).to eq(4) + expect(previous.revision(:previous).version).to eq(3) end it "should be able to set protected attributes" do @@ -330,14 +330,14 @@ class Secret u.update_attribute :logins, 1 u.update_attribute :logins, 2 - u.revision(3).logins.should be(2) - u.revision(2).logins.should be(1) - u.revision(1).logins.should be(0) + expect(u.revision(3).logins).to eq(2) + expect(u.revision(2).logins).to eq(1) + expect(u.revision(1).logins).to eq(0) end it "should set attributes directly" do u = Models::MongoMapper::User.create(:name => '') - u.revision(1).name.should == '<Joe>' + expect(u.revision(1).name).to eq('<Joe>') end it "should set the attributes for each revision" do @@ -345,20 +345,20 @@ class Secret u.update_attributes :name => 'Foobar' u.update_attributes :name => 'Awesome', :username => 'keepers' - u.revision(3).name.should == 'Awesome' - u.revision(3).username.should == 'keepers' + expect(u.revision(3).name).to eq('Awesome') + expect(u.revision(3).username).to eq('keepers') - u.revision(2).name.should == 'Foobar' - u.revision(2).username.should == 'brandon' + expect(u.revision(2).name).to eq('Foobar') + expect(u.revision(2).username).to eq('brandon') - u.revision(1).name.should == 'Brandon' - u.revision(1).username.should == 'brandon' + expect(u.revision(1).name).to eq('Brandon') + expect(u.revision(1).username).to eq('brandon') end it "should be able to get time for first revision" do suspended_at = Time.now.utc u = Models::MongoMapper::User.create(:suspended_at => suspended_at) - u.revision(1).suspended_at.to_i.should == suspended_at.to_i + expect(u.revision(1).suspended_at.to_i).to eq(suspended_at.to_i) end it "should not raise an error when no previous audits exist" do @@ -367,10 +367,11 @@ class Secret end it "should mark revision's attributes as changed" do - user.revision(1).name_changed?.should be_true + expect(user.revision(1).name_changed?).to eq(true) end it "should record new audit when saving revision" do + user.destroy expect { user.revision(1).save! }.to change( user.audits, :count ).by(1) @@ -392,11 +393,11 @@ class Secret audit.created_at = 1.hour.ago audit.save! user.update_attributes :name => 'updated' - user.revision_at( 2.minutes.ago ).version.should be(1) + expect(user.revision_at( 2.minutes.ago ).version).to eq(1) end it "should be nil if given a time before audits" do - user.revision_at( 1.week.ago ).should be_nil + expect(user.revision_at( 1.week.ago )).to be_nil end end @@ -404,7 +405,7 @@ class Secret it "should not save an audit when calling #save_without_auditing" do expect { u = Models::MongoMapper::User.new(:name => 'Brandon') - u.save_without_auditing.should be_true + expect(u.save_without_auditing).to eq(true) }.to_not change( Audited.audit_class, :count ) end @@ -419,16 +420,16 @@ class Secret describe "on create" do it "should not validate when audit_comment is not supplied" do - Models::MongoMapper::CommentRequiredUser.new.should_not be_valid + expect(Models::MongoMapper::CommentRequiredUser.new).not_to be_valid end it "should validate when audit_comment is supplied" do - Models::MongoMapper::CommentRequiredUser.new( :audit_comment => 'Create').should be_valid + expect(Models::MongoMapper::CommentRequiredUser.new( :audit_comment => 'Create')).to be_valid end it "should validate when audit_comment is not supplied, and auditing is disabled" do Models::MongoMapper::CommentRequiredUser.disable_auditing - Models::MongoMapper::CommentRequiredUser.new.should be_valid + expect(Models::MongoMapper::CommentRequiredUser.new).to be_valid Models::MongoMapper::CommentRequiredUser.enable_auditing end end @@ -437,16 +438,16 @@ class Secret let( :user ) { Models::MongoMapper::CommentRequiredUser.create!( :audit_comment => 'Create' ) } it "should not validate when audit_comment is not supplied" do - user.update_attributes(:name => 'Test').should be_false + expect(user.update_attributes(:name => 'Test')).to eq(false) end it "should validate when audit_comment is supplied" do - user.update_attributes(:name => 'Test', :audit_comment => 'Update').should be_true + expect(user.update_attributes(:name => 'Test', :audit_comment => 'Update')).to eq(true) end it "should validate when audit_comment is not supplied, and auditing is disabled" do Models::MongoMapper::CommentRequiredUser.disable_auditing - user.update_attributes(:name => 'Test').should be_true + expect(user.update_attributes(:name => 'Test')).to eq(true) Models::MongoMapper::CommentRequiredUser.enable_auditing end end @@ -455,17 +456,19 @@ class Secret let( :user ) { Models::MongoMapper::CommentRequiredUser.create!( :audit_comment => 'Create' )} it "should not validate when audit_comment is not supplied" do - user.destroy.should be_false + expect(user.destroy).to eq(false) end it "should validate when audit_comment is supplied" do user.audit_comment = "Destroy" - user.destroy.should == true + user.destroy + expect(user).to be_destroyed end it "should validate when audit_comment is not supplied, and auditing is disabled" do Models::MongoMapper::CommentRequiredUser.disable_auditing - user.destroy.should == true + user.destroy + expect(user).to be_destroyed Models::MongoMapper::CommentRequiredUser.enable_auditing end end @@ -496,7 +499,7 @@ class Secret company.update_attributes :name => 'The Auditors' company.audits.each do |audit| - audit.user.should == user + expect(audit.user).to eq(user) end end end @@ -507,7 +510,7 @@ class Secret company.update_attributes :name => 'The Auditors' company.audits.each do |audit| - audit.user.should == user.name + expect(audit.user).to eq(user.name) end end end @@ -517,10 +520,19 @@ class Secret let( :user ) { user = Models::MongoMapper::UserWithAfterAudit.new } it "should invoke after_audit callback on create" do - user.bogus_attr.should == nil - user.save.should == true - user.bogus_attr.should == "do something" + expect(user.bogus_attr).to be_nil + expect(user.save).to eq(true) + expect(user.bogus_attr).to eq("do something") end end + describe "around_audit" do + let( :user ) { user = Models::MongoMapper::UserWithAfterAudit.new } + + it "should invoke around_audit callback on create" do + expect(user.around_attr).to be_nil + expect(user.save).to eq(true) + expect(user.around_attr).to eq(user.audits.last) + end + end end diff --git a/spec/audited/adapters/mongo_mapper/mongo_mapper_spec_helper.rb b/spec/audited/adapters/mongo_mapper/mongo_mapper_spec_helper.rb index 0556676e..eaf15357 100644 --- a/spec/audited/adapters/mongo_mapper/mongo_mapper_spec_helper.rb +++ b/spec/audited/adapters/mongo_mapper/mongo_mapper_spec_helper.rb @@ -2,3 +2,4 @@ require 'support/mongo_mapper/connection' require 'audited/adapters/mongo_mapper' require 'support/mongo_mapper/models' +load "audited/sweeper.rb" # force to reload sweeper diff --git a/spec/audited/adapters/mongo_mapper/sweeper_spec.rb b/spec/audited/adapters/mongo_mapper/sweeper_spec.rb index 1befa6c8..31bbcee2 100644 --- a/spec/audited/adapters/mongo_mapper/sweeper_spec.rb +++ b/spec/audited/adapters/mongo_mapper/sweeper_spec.rb @@ -1,6 +1,6 @@ require File.expand_path('../mongo_mapper_spec_helper', __FILE__) -class AuditsController < ActionController::Base +class MongoAuditsController < ActionController::Base def audit @company = Models::MongoMapper::Company.create render :nothing => true @@ -17,7 +17,7 @@ def update_user attr_accessor :custom_user end -describe AuditsController, :adapter => :mongo_mapper do +describe MongoAuditsController, :adapter => :mongo_mapper do include RSpec::Rails::ControllerExampleGroup before(:each) do @@ -35,7 +35,7 @@ def update_user post :audit }.to change( Audited.audit_class, :count ) - assigns(:company).audits.last.user.should == user + expect(assigns(:company).audits.last.user).to eq(user) end it "should support custom users for sweepers" do @@ -46,7 +46,7 @@ def update_user post :audit }.to change( Audited.audit_class, :count ) - assigns(:company).audits.last.user.should == user + expect(assigns(:company).audits.last.user).to eq(user) end it "should record the remote address responsible for the change" do @@ -55,7 +55,16 @@ def update_user post :audit - assigns(:company).audits.last.remote_address.should == '1.2.3.4' + expect(assigns(:company).audits.last.remote_address).to eq('1.2.3.4') + end + + it "should record a UUID for the web request responsible for the change" do + allow_any_instance_of(ActionDispatch::Request).to receive(:uuid).and_return("abc123") + controller.send(:current_user=, user) + + post :audit + + expect(assigns(:company).audits.last.request_uuid).to eq("abc123") end end @@ -72,3 +81,26 @@ def update_user end end + + +describe Audited::Sweeper, :adapter => :mongo_mapper do + + it "should be thread-safe" do + t1 = Thread.new do + sleep 0.5 + Audited::Sweeper.instance.controller = 'thread1 controller instance' + expect(Audited::Sweeper.instance.controller).to eq('thread1 controller instance') + end + + t2 = Thread.new do + Audited::Sweeper.instance.controller = 'thread2 controller instance' + sleep 1 + expect(Audited::Sweeper.instance.controller).to eq('thread2 controller instance') + end + + t1.join; t2.join + + expect(Audited::Sweeper.instance.controller).to be_nil + end + +end diff --git a/spec/rails_app/config/application.rb b/spec/rails_app/config/application.rb index ef23ea67..0df2498d 100644 --- a/spec/rails_app/config/application.rb +++ b/spec/rails_app/config/application.rb @@ -1,5 +1,8 @@ +require 'rails/all' + module RailsApp class Application < Rails::Application config.root = File.expand_path('../../', __FILE__) + config.i18n.enforce_available_locales = true end end diff --git a/spec/rails_app/config/environments/development.rb b/spec/rails_app/config/environments/development.rb index b210e734..f9c62c39 100644 --- a/spec/rails_app/config/environments/development.rb +++ b/spec/rails_app/config/environments/development.rb @@ -7,7 +7,7 @@ config.cache_classes = false # Log error messages when you accidentally call methods on nil. - config.whiny_nils = true + # config.whiny_nils = true # Show full error reports and disable caching config.consider_all_requests_local = true @@ -16,4 +16,6 @@ # Don't care if the mailer can't send config.action_mailer.raise_delivery_errors = false + + config.eager_load = false end diff --git a/spec/rails_app/config/environments/production.rb b/spec/rails_app/config/environments/production.rb index fe0831be..44b5e8ab 100644 --- a/spec/rails_app/config/environments/production.rb +++ b/spec/rails_app/config/environments/production.rb @@ -30,4 +30,6 @@ # Enable threaded mode # config.threadsafe! + + config.eager_load = true end diff --git a/spec/rails_app/config/environments/test.rb b/spec/rails_app/config/environments/test.rb index 0dfc4389..49ba396b 100644 --- a/spec/rails_app/config/environments/test.rb +++ b/spec/rails_app/config/environments/test.rb @@ -8,7 +8,7 @@ config.cache_classes = true # Log error messages when you accidentally call methods on nil. - config.whiny_nils = true + # config.whiny_nils = true # Show full error reports and disable caching config.consider_all_requests_local = true @@ -30,4 +30,6 @@ config.action_dispatch.show_exceptions = false config.active_support.deprecation = :stderr + + config.eager_load = false end diff --git a/spec/rails_app/config/initializers/secret_token.rb b/spec/rails_app/config/initializers/secret_token.rb index 5d824ee9..79939e0f 100644 --- a/spec/rails_app/config/initializers/secret_token.rb +++ b/spec/rails_app/config/initializers/secret_token.rb @@ -1,2 +1,3 @@ Rails.application.config.secret_token = 'ea942c41850d502f2c8283e26bdc57829f471bb18224ddff0a192c4f32cdf6cb5aa0d82b3a7a7adbeb640c4b06f3aa1cd5f098162d8240f669b39d6b49680571' Rails.application.config.session_store :cookie_store, :key => "_my_app" +Rails.application.config.secret_key_base = 'secret value' diff --git a/spec/rails_app/config/routes.rb b/spec/rails_app/config/routes.rb index 56867c1e..55d13809 100644 --- a/spec/rails_app/config/routes.rb +++ b/spec/rails_app/config/routes.rb @@ -2,5 +2,5 @@ # This is a legacy wild controller route that's not recommended for RESTful applications. # Note: This route will make all actions in every controller accessible via GET requests. - match ':controller(/:action(/:id(.:format)))' + match ':controller(/:action(/:id(.:format)))', via: [:get, :post, :put, :delete] end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index dc0b6598..42d9215d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,6 @@ ENV['RAILS_ENV'] = 'test' -require 'rails/all' +require 'protected_attributes' require 'rails_app/config/environment' require 'rspec/rails' require 'audited' diff --git a/spec/support/active_record/models.rb b/spec/support/active_record/models.rb index 8ea5a54c..0e88029c 100644 --- a/spec/support/active_record/models.rb +++ b/spec/support/active_record/models.rb @@ -38,11 +38,15 @@ class NoAttributeProtectionUser < ::ActiveRecord::Base class UserWithAfterAudit < ::ActiveRecord::Base self.table_name = :users audited - attr_accessor :bogus_attr + attr_accessor :bogus_attr, :around_attr def after_audit self.bogus_attr = "do something" end + + def around_audit + self.around_attr = yield + end end class Company < ::ActiveRecord::Base @@ -52,6 +56,7 @@ class Company < ::ActiveRecord::Base class Owner < ::ActiveRecord::Base self.table_name = 'users' has_associated_audits + has_many :companies, class_name: "OwnedCompany", dependent: :destroy end class OwnedCompany < ::ActiveRecord::Base diff --git a/spec/support/active_record/schema.rb b/spec/support/active_record/schema.rb index d606a61b..1beca95b 100644 --- a/spec/support/active_record/schema.rb +++ b/spec/support/active_record/schema.rb @@ -44,11 +44,13 @@ t.column :version, :integer, :default => 0 t.column :comment, :string t.column :remote_address, :string + t.column :request_uuid, :string t.column :created_at, :datetime end add_index :audits, [:auditable_id, :auditable_type], :name => 'auditable_index' add_index :audits, [:associated_id, :associated_type], :name => 'associated_index' add_index :audits, [:user_id, :user_type], :name => 'user_index' + add_index :audits, :request_uuid add_index :audits, :created_at end diff --git a/spec/support/mongo_mapper/models.rb b/spec/support/mongo_mapper/models.rb index 8ee40acc..6bd0bfb1 100644 --- a/spec/support/mongo_mapper/models.rb +++ b/spec/support/mongo_mapper/models.rb @@ -94,11 +94,15 @@ class UserWithAfterAudit timestamps! audited - attr_accessor :bogus_attr + attr_accessor :bogus_attr, :around_attr def after_audit self.bogus_attr = "do something" end + + def around_audit + self.around_attr = yield + end end class Company diff --git a/test/db/version_6.rb b/test/db/version_6.rb new file mode 100644 index 00000000..96f59f05 --- /dev/null +++ b/test/db/version_6.rb @@ -0,0 +1,17 @@ +ActiveRecord::Schema.define do + create_table :audits, :force => true do |t| + t.column :auditable_id, :integer + t.column :auditable_type, :string + t.column :user_id, :integer + t.column :user_type, :string + t.column :username, :string + t.column :action, :string + t.column :audited_changes, :text + t.column :version, :integer, :default => 0 + t.column :comment, :string + t.column :created_at, :datetime + t.column :remote_address, :string + t.column :associated_id, :integer + t.column :associated_type, :string + end +end diff --git a/test/upgrade_generator_test.rb b/test/upgrade_generator_test.rb index 22ba016f..de56933c 100644 --- a/test/upgrade_generator_test.rb +++ b/test/upgrade_generator_test.rb @@ -1,5 +1,6 @@ require 'test_helper' +require 'audited/adapters/active_record' require 'generators/audited/upgrade_generator' class UpgradeGeneratorTest < Rails::Generators::TestCase @@ -62,4 +63,15 @@ class UpgradeGeneratorTest < Rails::Generators::TestCase assert_match /rename_column :audits, :association_type, :associated_type/, content end end + + test "should add 'request_uuid' to audits table" do + load_schema 6 + + run_generator %w(upgrade) + + assert_migration "db/migrate/add_request_uuid_to_audits.rb" do |content| + assert_match /add_column :audits, :request_uuid, :string/, content + assert_match /add_index :audits, :request_uuid/, content + end + end end