From eb131dc8c54a66da6cff1ea6ec13422913718535 Mon Sep 17 00:00:00 2001 From: Oshoma Momoh Date: Sun, 29 Mar 2009 18:04:56 -0700 Subject: [PATCH 01/12] Add :only option to #acts_as_audited to allow minimalist definition of attributes to audit. Example: class Meal < ActiveRecord::Base acts_as_audited :only => [:lunch, :dinner, :snacks] end --- lib/acts_as_audited.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/acts_as_audited.rb b/lib/acts_as_audited.rb index 3347ca00..949c46d0 100644 --- a/lib/acts_as_audited.rb +++ b/lib/acts_as_audited.rb @@ -40,11 +40,12 @@ def self.included(base) # :nodoc: module ClassMethods # == Configuration options # + # + # * +only+ - Only audit the given attributes # * +except+ - Excludes fields from being saved in the audit log. # By default, acts_as_audited will audit all but these fields: # # [self.primary_key, inheritance_column, 'lock_version', 'created_at', 'updated_at'] - # # You can add to those by passing one or an array of fields to skip. # # class User < ActiveRecord::Base @@ -68,9 +69,13 @@ def acts_as_audited(options = {}) class_inheritable_reader :non_audited_columns class_inheritable_reader :auditing_enabled - - except = [self.primary_key, inheritance_column, 'lock_version', 'created_at', 'updated_at'] - except |= Array(options[:except]).collect(&:to_s) if options[:except] + + if options[:only] + except = self.column_names - options[:only].flatten.map(&:to_s) + else + except = [self.primary_key, inheritance_column, 'lock_version', 'created_at', 'updated_at'] + except |= Array(options[:except]).collect(&:to_s) if options[:except] + end write_inheritable_attribute :non_audited_columns, except has_many :audits, :as => :auditable, :order => "#{Audit.quoted_table_name}.version" From 5cc0bf00abdaca1f023282f75d31919c1e68c75a Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Sat, 23 May 2009 17:51:08 -0400 Subject: [PATCH 02/12] Switch back to TestUnit with Shoulda to get controller tests --- .gitignore | 2 +- Rakefile | 10 +- spec/acts_as_audited_spec.rb | 311 ---------------------- spec/audit_sweeper_spec.rb | 38 --- spec/spec.opts | 4 - spec/spec_helper.rb | 48 ---- test/acts_as_audited_test.rb | 314 +++++++++++++++++++++++ test/audit_sweeper_test.rb | 36 +++ spec/audit_spec.rb => test/audit_test.rb | 50 ++-- {spec => test/db}/database.yml | 0 {spec => test/db}/schema.rb | 0 test/test_helper.rb | 132 ++++++++++ 12 files changed, 513 insertions(+), 432 deletions(-) delete mode 100644 spec/acts_as_audited_spec.rb delete mode 100644 spec/audit_sweeper_spec.rb delete mode 100644 spec/spec.opts delete mode 100644 spec/spec_helper.rb create mode 100644 test/acts_as_audited_test.rb create mode 100644 test/audit_sweeper_test.rb rename spec/audit_spec.rb => test/audit_test.rb (64%) rename {spec => test/db}/database.yml (100%) rename {spec => test/db}/schema.rb (100%) create mode 100644 test/test_helper.rb diff --git a/.gitignore b/.gitignore index e9c0a9ee..3d85cc6c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ acts_as_audited_plugin.sqlite3.db -spec/debug.log +test/debug.log coverage/ \ No newline at end of file diff --git a/Rakefile b/Rakefile index f8e22b6c..72a53c38 100644 --- a/Rakefile +++ b/Rakefile @@ -1,14 +1,14 @@ require 'rake' -require 'spec/rake/spectask' +require 'rake/testtask' require 'rake/rdoctask' -desc 'Default: run specs.' -task :default => :spec +desc 'Default: run tests.' +task :default => :test desc 'Test the acts_as_audited plugin' -Spec::Rake::SpecTask.new(:spec) do |t| +Rake::TestTask.new(:test) do |t| t.libs << 'lib' - t.pattern = 'spec/**/*_spec.rb' + t.pattern = 'test/**/*_test.rb' t.verbose = true end diff --git a/spec/acts_as_audited_spec.rb b/spec/acts_as_audited_spec.rb deleted file mode 100644 index ebd52b41..00000000 --- a/spec/acts_as_audited_spec.rb +++ /dev/null @@ -1,311 +0,0 @@ -require File.expand_path(File.dirname(__FILE__) + '/spec_helper') - -describe CollectiveIdea::Acts::Audited do - - it "should include instance methods" do - User.new.should be_kind_of(CollectiveIdea::Acts::Audited::InstanceMethods) - end - - it "should extend singleton methods" do - User.should be_kind_of(CollectiveIdea::Acts::Audited::SingletonMethods) - end - - ['created_at', 'updated_at', 'lock_version', 'id', 'password'].each do |column| - it "should not audit #{column}" do - User.non_audited_columns.should include(column) - end - end - - it "should not save non-audited columns" do - create_user.audits.first.changes.keys.any?{|col| ['created_at', 'updated_at', 'password'].include? col}.should be_false - end - - describe "on create" do - it "should save an audit" do - lambda { - create_user.should have(1).audit - }.should change { Audit.count }.by(1) - end - - it "should set the action to 'create'" do - audit = create_user.audits.first - audit.action.should == 'create' - end - - it "should store all the audited attributes" do - user = User.create(:name => 'Brandon') - user.audits.first.changes.should == user.audited_attributes - end - end - - describe "on update" do - before do - @user = create_user(:name => 'Brandon') - end - - it "should save an audit on update" do - lambda { @user.update_attribute(:name, "Someone") }.should change { @user.audits.count }.by(1) - lambda { @user.update_attribute(:name, "Someone else") }.should change { @user.audits.count }.by(1) - end - - it "should not save an audit if the record is not changed" do - lambda { @user.save! }.should_not change { Audit.count } - end - - it "should set the action to 'update'" do - @user.update_attributes :name => 'Changed' - @user.audits.last.action.should == 'update' - end - - it "should store the changed attributes" do - @user.update_attributes :name => 'Changed' - @user.audits.last.changes.should == {'name' => ['Brandon', 'Changed']} - end - - it "should not save an audit if the value doesn't change after type casting" do - @user.update_attributes! :logins => 0, :activated => true - lambda { @user.update_attribute :logins, '0' }.should_not change { Audit.count } - lambda { @user.update_attribute :activated, 1 }.should_not change { Audit.count } - lambda { @user.update_attribute :activated, '1' }.should_not change { Audit.count } - end - - end - - describe "on destroy" do - before do - @user = create_user - end - - it "should save an audit" do - lambda { @user.destroy }.should change { Audit.count }.by(1) - @user.should have(2).audits - end - - it "should set the action to 'destroy'" do - @user.destroy - @user.audits.last.action.should == 'destroy' - end - - it "should store all of the audited attributes" do - @user.destroy - @user.audits.last.changes.should == @user.audited_attributes - end - - it "should be able to reconstruct destroyed record without history" do - @user.audits.delete_all - @user.destroy - revision = @user.audits.first.revision - revision.name.should == @user.name - end - end - - describe "dirty tracking" do - before do - @user = create_user - end - - it "should not be changed when the record is saved" do - u = User.new(:name => 'Brandon') - u.should be_changed - u.save - u.should_not be_changed - end - - it "should be changed when an attribute has been changed" do - @user.name = "Bobby" - @user.should be_changed - @user.name_changed?.should be_true - @user.username_changed?.should be_false - end - - it "should not be changed if the value doesn't change after type casting" do - @user.update_attributes! :logins => 0, :activated => true - @user.logins = '0' - @user.should_not be_changed - end - - end - - describe "revisions" do - before do - @user = create_versions - end - - it "should be an Array of Users" do - @user.revisions.should be_an_instance_of(Array) - @user.revisions.each {|version| version.should be_an_instance_of(User) } - end - - it "should have one revision for a new record" do - create_user.revisions.size.should == 1 - end - - it "should have one revision for each audit" do - @user.should have(@user.audits.size).revisions - end - - it "should set the attributes for each revision" do - u = User.create(:name => 'Brandon', :username => 'brandon') - u.update_attributes :name => 'Foobar' - u.update_attributes :name => 'Awesome', :username => 'keepers' - - u.revisions.size.should == 3 - - u.revisions[0].name.should == 'Brandon' - u.revisions[0].username.should == 'brandon' - - u.revisions[1].name.should == 'Foobar' - u.revisions[1].username.should == 'brandon' - - u.revisions[2].name.should == 'Awesome' - u.revisions[2].username.should == 'keepers' - end - - it "should access to only recent revisions" do - u = User.create(:name => 'Brandon', :username => 'brandon') - u.update_attributes :name => 'Foobar' - u.update_attributes :name => 'Awesome', :username => 'keepers' - - u.revisions(2).size.should == 2 - - u.revisions(2)[0].name.should == 'Foobar' - u.revisions(2)[0].username.should == 'brandon' - - u.revisions(2)[1].name.should == 'Awesome' - u.revisions(2)[1].username.should == 'keepers' - end - - it "should be empty if no audits exist" do - @user.audits.delete_all - @user.revisions.should be_empty - end - - it "should ignore attributes that have been deleted" do - @user.audits.last.update_attributes :changes => {:old_attribute => 'old value'} - lambda { @user.revisions }.should_not raise_error(ActiveRecord::UnknownAttributeError) - end - - end - - describe "revision" do - before do - @user = create_versions(5) - end - - it "should maintain identity" do - @user.revision(1).should == @user - end - - it "should find the given revision" do - revision = @user.revision(3) - revision.should be_an_instance_of(User) - revision.version.should == 3 - revision.name.should == 'Foobar 3' - end - - it "should find the previous revision with :previous" do - revision = @user.revision(:previous) - revision.version.should == 4 - revision.should == @user.revision(4) - end - - it "should be able to get the previous revision repeatedly" do - previous = @user.revision(:previous) - previous.version.should == 4 - previous.revision(:previous).version.should == 3 - end - - it "should set the attributes for each revision" do - u = User.create(:name => 'Brandon', :username => 'brandon') - u.update_attributes :name => 'Foobar' - u.update_attributes :name => 'Awesome', :username => 'keepers' - - u.revision(3).name.should == 'Awesome' - u.revision(3).username.should == 'keepers' - - u.revision(2).name.should == 'Foobar' - u.revision(2).username.should == 'brandon' - - u.revision(1).name.should == 'Brandon' - u.revision(1).username.should == 'brandon' - end - - it "should not raise an error when no previous audits exist" do - @user.audits.destroy_all - lambda{ @user.revision(:previous) }.should_not raise_error - end - - it "should mark revision's attributes as changed" do - @user.revision(1).name_changed?.should be_true - end - - it "should record new audit when saving revision" do - lambda { @user.revision(1).save! }.should change { @user.audits.count } - end - - end - - describe "revision_at" do - it "should find the latest revision before the given time" do - u = create_user - Audit.update(u.audits.first.id, :created_at => 1.hour.ago) - u.update_attributes :name => 'updated' - u.revision_at(2.minutes.ago).version.should == 1 - end - - it "should be nil if given a time before audits" do - create_user.revision_at(1.week.ago).should be_nil - end - - end - - describe "without auditing" do - - it "should not save an audit when calling #save_without_auditing" do - lambda { - u = User.new(:name => 'Brandon') - u.save_without_auditing.should be_true - }.should_not change { Audit.count } - end - - it "should not save an audit inside of the #without_auditing block" do - lambda do - User.without_auditing { User.create(:name => 'Brandon') } - end.should_not change { Audit.count } - end - - it "should not save an audit when callbacks are disabled" do - begin - User.disable_auditing_callbacks - lambda { create_user }.should_not change { Audit.count } - ensure - User.enable_auditing_callbacks - end - end - end - - describe "attr_protected and attr_accessible" do - class UnprotectedUser < ActiveRecord::Base - set_table_name :users - acts_as_audited :protect => false - attr_accessible :name, :username, :password - end - it "should not raise error when attr_accessible is set and protected is false" do - lambda{ - UnprotectedUser.new(:name => 'NO FAIL!') - }.should_not raise_error(RuntimeError) - end - - class AccessibleUser < ActiveRecord::Base - set_table_name :users - attr_accessible :name, :username, :password # declare attr_accessible before calling aaa - acts_as_audited - end - it "should not raise an error when attr_accessible is declared before acts_as_audited" do - lambda{ - AccessibleUser.new(:name => 'NO FAIL!') - }.should_not raise_error - end - end - -end diff --git a/spec/audit_sweeper_spec.rb b/spec/audit_sweeper_spec.rb deleted file mode 100644 index 7c86a841..00000000 --- a/spec/audit_sweeper_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# require File.expand_path(File.dirname(__FILE__) + '/spec_helper') -# require 'spec/rails' -# -# class AuditsController < ActionController::Base -# audit Company -# attr_accessor :current_user -# -# def audit -# @company = Company.create -# render :nothing => true -# end -# -# end -# AuditsController.view_paths = [File.expand_path(File.dirname(__FILE__) + "/../fixtures")] -# ActionController::Routing::Routes.draw {|m| m.connect ':controller/:action/:id' } -# -# describe "AuditSweeper" do -# before do -# @controller = AuditsController.new -# @controller.logger = Logger.new(nil) -# @request = ActionController::TestRequest.new -# @response = ActionController::TestResponse.new -# @request.host = "www.example.com" -# end -# -# it "calls acts as audited on non audited models" do -# Company.should be_kind_of(CollectiveIdea::Acts::Audited::SingletonMethods) -# end -# -# it "audits user" do -# user = @controller.current_user = create_user -# assert_difference Audit, :count do -# post :audit -# end -# assigns(:company).audits.last.user.should == user -# end -# -# end \ No newline at end of file diff --git a/spec/spec.opts b/spec/spec.opts deleted file mode 100644 index 391705bf..00000000 --- a/spec/spec.opts +++ /dev/null @@ -1,4 +0,0 @@ ---colour ---format progress ---loadby mtime ---reverse diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index 51d3c943..00000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,48 +0,0 @@ -ENV["RAILS_ENV"] = "test" -$:.unshift(File.dirname(__FILE__) + '/../lib') -require 'rubygems' -require 'active_record' -require 'action_controller' -require 'action_view' -require File.dirname(__FILE__) + '/../init.rb' - -require 'active_record/fixtures' -require 'action_controller/test_process' - -require 'spec' - -config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')) -ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log") -ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite3mem']) - -ActiveRecord::Migration.verbose = false -load(File.dirname(__FILE__) + "/schema.rb") - -class User < ActiveRecord::Base - acts_as_audited :except => :password -end -class Company < ActiveRecord::Base -end - - -Spec::Runner.configure do |config| - # config.use_transactional_fixtures = true - # config.use_instantiated_fixtures = false - # config.fixture_path = '/spec/fixtures/' - - def create_user(attrs = {}) - User.create({:name => 'Brandon', :username => 'brandon', :password => 'password'}.merge(attrs)) - end - - def create_versions(n = 2) - returning User.create(:name => 'Foobar 1') do |u| - (n - 1).times do |i| - u.update_attribute :name, "Foobar #{i + 2}" - end - u.reload - end - - end - -end - diff --git a/test/acts_as_audited_test.rb b/test/acts_as_audited_test.rb new file mode 100644 index 00000000..7e14deb2 --- /dev/null +++ b/test/acts_as_audited_test.rb @@ -0,0 +1,314 @@ +require File.expand_path(File.dirname(__FILE__) + '/test_helper') + +module CollectiveIdea + module Acts + class AuditedTest < Test::Unit::TestCase + should "include instance methods" do + User.new.should be_kind_of(CollectiveIdea::Acts::Audited::InstanceMethods) + end + + should "extend singleton methods" do + User.should be_kind_of(CollectiveIdea::Acts::Audited::SingletonMethods) + end + + ['created_at', 'updated_at', 'lock_version', 'id', 'password'].each do |column| + should "not audit #{column}" do + User.non_audited_columns.should include(column) + end + end + + should "not save non-audited columns" do + create_user.audits.first.changes.keys.any?{|col| ['created_at', 'updated_at', 'password'].include? col}.should be(false) + end + + context "on create" do + setup { @user = create_user } + + should_change 'Audit.count', :by => 1 + + should 'create associated audit' do + @user.audits.count.should == 1 + end + + should "set the action to 'create'" do + @user.audits.first.action.should == 'create' + end + + should "store all the audited attributes" do + @user.audits.first.changes.should == @user.audited_attributes + end + end + + context "on update" do + setup do + @user = create_user(:name => 'Brandon') + end + + should "save an audit" do + lambda { @user.update_attribute(:name, "Someone") }.should change { @user.audits.count }.by(1) + lambda { @user.update_attribute(:name, "Someone else") }.should change { @user.audits.count }.by(1) + end + + should "not save an audit if the record is not changed" do + lambda { @user.save! }.should_not change { Audit.count } + end + + should "set the action to 'update'" do + @user.update_attributes :name => 'Changed' + @user.audits.last.action.should == 'update' + end + + should "store the changed attributes" do + @user.update_attributes :name => 'Changed' + @user.audits.last.changes.should == {'name' => ['Brandon', 'Changed']} + end + + should "not save an audit if the value doesn't change after type casting" do + @user.update_attributes! :logins => 0, :activated => true + lambda { @user.update_attribute :logins, '0' }.should_not change { Audit.count } + lambda { @user.update_attribute :activated, 1 }.should_not change { Audit.count } + lambda { @user.update_attribute :activated, '1' }.should_not change { Audit.count } + end + + end + + context "on destroy" do + setup do + @user = create_user + end + + should "save an audit" do + lambda { @user.destroy }.should change { Audit.count }.by(1) + @user.audits.size.should == 2 + end + + should "set the action to 'destroy'" do + @user.destroy + @user.audits.last.action.should == 'destroy' + end + + should "store all of the audited attributes" do + @user.destroy + @user.audits.last.changes.should == @user.audited_attributes + end + + should "be able to reconstruct destroyed record without history" do + @user.audits.delete_all + @user.destroy + revision = @user.audits.first.revision + revision.name.should == @user.name + end + end + + context "dirty tracking" do + setup do + @user = create_user + end + + should "not be changed when the record is saved" do + u = User.new(:name => 'Brandon') + u.changed?.should be(true) + u.save + u.changed?.should be(false) + end + + should "be changed when an attribute has been changed" do + @user.name = "Bobby" + @user.changed?.should be(true) + @user.name_changed?.should be(true) + @user.username_changed?.should be(false) + end + + should "not be changed if the value doesn't change after type casting" do + @user.update_attributes! :logins => 0, :activated => true + @user.logins = '0' + @user.changed?.should be(false) + end + + end + + context "revisions" do + setup do + @user = create_versions + end + + should "be an Array of Users" do + @user.revisions.should be_kind_of(Array) + @user.revisions.each {|version| version.should be_kind_of(User) } + end + + should "have one revision for a new record" do + create_user.revisions.size.should == 1 + end + + should "have one revision for each audit" do + @user.revisions.size.should == @user.audits.size + end + + should "set the attributes for each revision" do + u = User.create(:name => 'Brandon', :username => 'brandon') + u.update_attributes :name => 'Foobar' + u.update_attributes :name => 'Awesome', :username => 'keepers' + + u.revisions.size.should == 3 + + u.revisions[0].name.should == 'Brandon' + u.revisions[0].username.should == 'brandon' + + u.revisions[1].name.should == 'Foobar' + u.revisions[1].username.should == 'brandon' + + u.revisions[2].name.should == 'Awesome' + u.revisions[2].username.should == 'keepers' + end + + should "access to only recent revisions" do + u = User.create(:name => 'Brandon', :username => 'brandon') + u.update_attributes :name => 'Foobar' + u.update_attributes :name => 'Awesome', :username => 'keepers' + + u.revisions(2).size.should == 2 + + u.revisions(2)[0].name.should == 'Foobar' + u.revisions(2)[0].username.should == 'brandon' + + u.revisions(2)[1].name.should == 'Awesome' + u.revisions(2)[1].username.should == 'keepers' + end + + should "be empty if no audits exist" do + @user.audits.delete_all + @user.revisions.empty?.should be(true) + end + + should "ignore attributes that have been deleted" do + @user.audits.last.update_attributes :changes => {:old_attribute => 'old value'} + lambda { @user.revisions }.should_not raise_error(ActiveRecord::UnknownAttributeError) + end + + end + + context "revision" do + setup do + @user = create_versions(5) + end + + should "maintain identity" do + @user.revision(1).should == @user + end + + should "find the given revision" do + revision = @user.revision(3) + revision.should be_kind_of(User) + revision.version.should == 3 + revision.name.should == 'Foobar 3' + end + + should "find the previous revision with :previous" do + revision = @user.revision(:previous) + revision.version.should == 4 + revision.should == @user.revision(4) + end + + should "be able to get the previous revision repeatedly" do + previous = @user.revision(:previous) + previous.version.should == 4 + previous.revision(:previous).version.should == 3 + end + + should "set the attributes for each revision" do + u = User.create(:name => 'Brandon', :username => 'brandon') + u.update_attributes :name => 'Foobar' + u.update_attributes :name => 'Awesome', :username => 'keepers' + + u.revision(3).name.should == 'Awesome' + u.revision(3).username.should == 'keepers' + + u.revision(2).name.should == 'Foobar' + u.revision(2).username.should == 'brandon' + + u.revision(1).name.should == 'Brandon' + u.revision(1).username.should == 'brandon' + end + + should "not raise an error when no previous audits exist" do + @user.audits.destroy_all + lambda{ @user.revision(:previous) }.should_not raise_error + end + + should "mark revision's attributes as changed" do + @user.revision(1).name_changed?.should be(true) + end + + should "record new audit when saving revision" do + lambda { @user.revision(1).save! }.should change { @user.audits.count }.by(1) + end + + end + + context "revision_at" do + should "find the latest revision before the given time" do + u = create_user + Audit.update(u.audits.first.id, :created_at => 1.hour.ago) + u.update_attributes :name => 'updated' + u.revision_at(2.minutes.ago).version.should == 1 + end + + should "be nil if given a time before audits" do + create_user.revision_at(1.week.ago).should be(nil) + end + + end + + context "without auditing" do + + should "not save an audit when calling #save_without_auditing" do + lambda { + u = User.new(:name => 'Brandon') + u.save_without_auditing.should be(true) + }.should_not change { Audit.count } + end + + should "not save an audit inside of the #without_auditing block" do + lambda do + User.without_auditing { User.create(:name => 'Brandon') } + end.should_not change { Audit.count } + end + + should "not save an audit when callbacks are disabled" do + begin + User.disable_auditing_callbacks + lambda { create_user }.should_not change { Audit.count } + ensure + User.enable_auditing_callbacks + end + end + end + + context "attr_protected and attr_accessible" do + class UnprotectedUser < ActiveRecord::Base + set_table_name :users + acts_as_audited :protect => false + attr_accessible :name, :username, :password + end + should "not raise error when attr_accessible is set and protected is false" do + lambda{ + UnprotectedUser.new(:name => 'NO FAIL!') + }.should_not raise_error(RuntimeError) + end + + class AccessibleUser < ActiveRecord::Base + set_table_name :users + attr_accessible :name, :username, :password # declare attr_accessible before calling aaa + acts_as_audited + end + should "not raise an error when attr_accessible is declared before acts_as_audited" do + lambda{ + AccessibleUser.new(:name => 'NO FAIL!') + }.should_not raise_error + end + end + + end + end +end \ No newline at end of file diff --git a/test/audit_sweeper_test.rb b/test/audit_sweeper_test.rb new file mode 100644 index 00000000..26ada89b --- /dev/null +++ b/test/audit_sweeper_test.rb @@ -0,0 +1,36 @@ +require File.expand_path(File.dirname(__FILE__) + '/test_helper') + +class AuditsController < ActionController::Base + audit Company + attr_accessor :current_user + + def audit + @company = Company.create + render :nothing => true + end + +end +AuditsController.view_paths = [File.dirname(__FILE__)] +ActionController::Routing::Routes.draw {|m| m.connect ':controller/:action/:id' } + +class AuditSweeperTest < ActionController::TestCase + + def setup + @controller = AuditsController.new + @controller.logger = Logger.new(nil) + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @request.host = "www.example.com" + end + + should "call acts as audited on non audited models" do + Company.should be_kind_of(CollectiveIdea::Acts::Audited::SingletonMethods) + end + + should "audit user" do + user = @controller.current_user = create_user + lambda { post :audit }.should change { Audit.count } + assigns(:company).audits.last.user.should == user + end + +end \ No newline at end of file diff --git a/spec/audit_spec.rb b/test/audit_test.rb similarity index 64% rename from spec/audit_spec.rb rename to test/audit_test.rb index 3f67358e..bc5bd5fd 100644 --- a/spec/audit_spec.rb +++ b/test/audit_test.rb @@ -1,18 +1,18 @@ -require File.expand_path(File.dirname(__FILE__) + '/spec_helper') +require File.expand_path(File.dirname(__FILE__) + '/test_helper') -describe Audit do - before do +class AuditTest < Test::Unit::TestCase + def setup @user = User.new :name => "testing" @audit = Audit.new end - describe "user=" do - it "should be able to set the user to a model object" do + context "user=" do + should "be able to set the user to a model object" do @audit.user = @user @audit.user.should == @user end - it "should be able to set the user to nil" do + should "be able to set the user to nil" do @audit.user_id = 1 @audit.user_type = 'User' @audit.username = 'joe' @@ -25,27 +25,27 @@ @audit.username.should == nil end - it "should be able to set the user to a string" do + should "be able to set the user to a string" do @audit.user = 'testing' @audit.user.should == 'testing' end - it "should clear model when setting to a string" do + should "clear model when setting to a string" do @audit.user = @user @audit.user = 'testing' - @audit.user_id.should be_nil - @audit.user_type.should be_nil + @audit.user_id.should be(nil) + @audit.user_type.should be(nil) end - it "should clear the username when setting to a model" do + should "clear the username when setting to a model" do @audit.username = 'testing' @audit.user = @user - @audit.username.should be_nil + @audit.username.should be(nil) end end - it "revision" do + should "revision" do user = User.create :name => "1" 5.times {|i| user.update_attribute :name, (i + 2).to_s } user.audits.each do |audit| @@ -53,15 +53,15 @@ end end - it "should be able to create revision for deleted records" do + should "be able to create revision for deleted records" do user = User.create :name => "1" user.destroy revision = user.audits.last.revision revision.name.should == user.name - revision.should be_new_record + revision.new_record?.should be(true) end - it "should set the version number on create" do + should "set the version number on create" do user = User.create! :name => "Set Version Number" user.audits.first.version.should == 1 user.update_attribute :name, "Set to 2" @@ -71,37 +71,37 @@ user.audits(true).last.version.should == 3 end - describe "reconstruct_attributes" do - it "should work with with old way of storing just the new value" do + context "reconstruct_attributes" do + should "work with with old way of storing just the new value" do audits = Audit.reconstruct_attributes([Audit.new(:changes => {'attribute' => 'value'})]) audits['attribute'].should == 'value' end end - describe "audited_classes" do + context "audited_classes" do class CustomUser < ActiveRecord::Base end class CustomUserSubclass < CustomUser acts_as_audited end - it "should include audited classes" do + should "include audited classes" do Audit.audited_classes.should include(User) end - it "should include subclasses" do + should "include subclasses" do Audit.audited_classes.should include(CustomUserSubclass) end end - describe "new_attributes" do - it "should return a hash of the new values" do + context "new_attributes" do + should "return a hash of the new values" do Audit.new(:changes => {:a => [1, 2], :b => [3, 4]}).new_attributes.should == {'a' => 2, 'b' => 4} end end - describe "old_attributes" do - it "should return a hash of the old values" do + context "old_attributes" do + should "return a hash of the old values" do Audit.new(:changes => {:a => [1, 2], :b => [3, 4]}).old_attributes.should == {'a' => 1, 'b' => 3} end end diff --git a/spec/database.yml b/test/db/database.yml similarity index 100% rename from spec/database.yml rename to test/db/database.yml diff --git a/spec/schema.rb b/test/db/schema.rb similarity index 100% rename from spec/schema.rb rename to test/db/schema.rb diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 00000000..d91a4694 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,132 @@ +ENV["RAILS_ENV"] = "test" +$:.unshift(File.dirname(__FILE__) + '/../lib') +require 'rubygems' +require 'active_record' +require 'active_record/fixtures' +require 'action_controller' +require 'action_controller/test_process' +require 'action_view' +require 'test/unit' +require 'shoulda' +require 'matchy' +require File.dirname(__FILE__) + '/../init.rb' + +config = YAML::load(IO.read(File.dirname(__FILE__) + '/db/database.yml')) +ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log") +ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite3mem']) +ActiveRecord::Migration.verbose = false +load(File.dirname(__FILE__) + "/db/schema.rb") + +class User < ActiveRecord::Base + acts_as_audited :except => :password +end +class Company < ActiveRecord::Base +end + +class Test::Unit::TestCase + def change(receiver=nil, message=nil, &block) + ChangeExpectation.new(self, receiver, message, &block) + end + + def create_user(attrs = {}) + User.create({:name => 'Brandon', :username => 'brandon', :password => 'password'}.merge(attrs)) + end + + def create_versions(n = 2) + returning User.create(:name => 'Foobar 1') do |u| + (n - 1).times do |i| + u.update_attribute :name, "Foobar #{i + 2}" + end + u.reload + end + end +end + +# Change matcher copied from RSpec +class ChangeExpectation < Matchy::Expectations::Base + def initialize(test_case, receiver=nil, message=nil, &block) + @test_case = test_case + @message = message || "result" + @value_proc = block || lambda {receiver.__send__(message)} + @to = @from = @minimum = @maximum = @amount = nil + end + + def matches?(event_proc) + raise_block_syntax_error if block_given? + + @before = evaluate_value_proc + event_proc.call + @after = evaluate_value_proc + + return (@to = false) if @from unless @from == @before + return false if @to unless @to == @after + return (@before + @amount == @after) if @amount + return ((@after - @before) >= @minimum) if @minimum + return ((@after - @before) <= @maximum) if @maximum + return @before != @after + end + + def raise_block_syntax_error + raise(<<-MESSAGE +block passed to should or should_not change must use {} instead of do/end +MESSAGE + ) + end + + def evaluate_value_proc + @value_proc.call + end + + def failure_message + if @to + "#{@message} should have been changed to #{@to.inspect}, but is now #{@after.inspect}" + elsif @from + "#{@message} should have initially been #{@from.inspect}, but was #{@before.inspect}" + elsif @amount + "#{@message} should have been changed by #{@amount.inspect}, but was changed by #{actual_delta.inspect}" + elsif @minimum + "#{@message} should have been changed by at least #{@minimum.inspect}, but was changed by #{actual_delta.inspect}" + elsif @maximum + "#{@message} should have been changed by at most #{@maximum.inspect}, but was changed by #{actual_delta.inspect}" + else + "#{@message} should have changed, but is still #{@before.inspect}" + end + end + + def actual_delta + @after - @before + end + + def negative_failure_message + "#{@message} should not have changed, but did change from #{@before.inspect} to #{@after.inspect}" + end + + def by(amount) + @amount = amount + self + end + + def by_at_least(minimum) + @minimum = minimum + self + end + + def by_at_most(maximum) + @maximum = maximum + self + end + + def to(to) + @to = to + self + end + + def from (from) + @from = from + self + end + + def description + "change ##{@message}" + end +end From 5e5a32465a4646e668b4dab87a80d795cbfc6d8c Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Sat, 23 May 2009 22:12:43 -0400 Subject: [PATCH 03/12] Add multi_rails again --- Rakefile | 1 + test/acts_as_audited_test.rb | 24 +++++++++++++++--------- test/audit_sweeper_test.rb | 10 +--------- test/test_helper.rb | 2 ++ 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/Rakefile b/Rakefile index 72a53c38..19d43545 100644 --- a/Rakefile +++ b/Rakefile @@ -1,4 +1,5 @@ require 'rake' +require 'load_multi_rails_rake_tasks' require 'rake/testtask' require 'rake/rdoctask' diff --git a/test/acts_as_audited_test.rb b/test/acts_as_audited_test.rb index 7e14deb2..fa47f022 100644 --- a/test/acts_as_audited_test.rb +++ b/test/acts_as_audited_test.rb @@ -63,11 +63,14 @@ class AuditedTest < Test::Unit::TestCase @user.audits.last.changes.should == {'name' => ['Brandon', 'Changed']} end - should "not save an audit if the value doesn't change after type casting" do - @user.update_attributes! :logins => 0, :activated => true - lambda { @user.update_attribute :logins, '0' }.should_not change { Audit.count } - lambda { @user.update_attribute :activated, 1 }.should_not change { Audit.count } - lambda { @user.update_attribute :activated, '1' }.should_not change { Audit.count } + # Dirty tracking in Rails 2.0-2.2 had issues with type casting + if ActiveRecord::VERSION::STRING >= '2.3' + should "not save an audit if the value doesn't change after type casting" do + @user.update_attributes! :logins => 0, :activated => true + lambda { @user.update_attribute :logins, '0' }.should_not change { Audit.count } + lambda { @user.update_attribute :activated, 1 }.should_not change { Audit.count } + lambda { @user.update_attribute :activated, '1' }.should_not change { Audit.count } + end end end @@ -119,10 +122,13 @@ class AuditedTest < Test::Unit::TestCase @user.username_changed?.should be(false) end - should "not be changed if the value doesn't change after type casting" do - @user.update_attributes! :logins => 0, :activated => true - @user.logins = '0' - @user.changed?.should be(false) + # Dirty tracking in Rails 2.0-2.2 had issues with type casting + if ActiveRecord::VERSION::STRING >= '2.3' + should "not be changed if the value doesn't change after type casting" do + @user.update_attributes! :logins => 0, :activated => true + @user.logins = '0' + @user.changed?.should be(false) + end end end diff --git a/test/audit_sweeper_test.rb b/test/audit_sweeper_test.rb index 26ada89b..ec339d9d 100644 --- a/test/audit_sweeper_test.rb +++ b/test/audit_sweeper_test.rb @@ -13,16 +13,8 @@ def audit AuditsController.view_paths = [File.dirname(__FILE__)] ActionController::Routing::Routes.draw {|m| m.connect ':controller/:action/:id' } -class AuditSweeperTest < ActionController::TestCase +class AuditsControllerTest < ActionController::TestCase - def setup - @controller = AuditsController.new - @controller.logger = Logger.new(nil) - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - @request.host = "www.example.com" - end - should "call acts as audited on non audited models" do Company.should be_kind_of(CollectiveIdea::Acts::Audited::SingletonMethods) end diff --git a/test/test_helper.rb b/test/test_helper.rb index d91a4694..fd770ad9 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,7 +1,9 @@ ENV["RAILS_ENV"] = "test" $:.unshift(File.dirname(__FILE__) + '/../lib') require 'rubygems' +require 'multi_rails_init' require 'active_record' +require 'active_record/version' require 'active_record/fixtures' require 'action_controller' require 'action_controller/test_process' From aa08f06b237e674aa080d528bf00de550f494809 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Sat, 23 May 2009 22:14:59 -0400 Subject: [PATCH 04/12] Avoid referencing error that was not defined in Rails 2.1 in test --- test/acts_as_audited_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/acts_as_audited_test.rb b/test/acts_as_audited_test.rb index fa47f022..d41a215c 100644 --- a/test/acts_as_audited_test.rb +++ b/test/acts_as_audited_test.rb @@ -189,7 +189,7 @@ class AuditedTest < Test::Unit::TestCase should "ignore attributes that have been deleted" do @user.audits.last.update_attributes :changes => {:old_attribute => 'old value'} - lambda { @user.revisions }.should_not raise_error(ActiveRecord::UnknownAttributeError) + lambda { @user.revisions }.should_not raise_error end end From 39dc47fd5620ca45455a7f9376ed5b0a93fed4cf Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Sat, 23 May 2009 22:26:08 -0400 Subject: [PATCH 05/12] Update README about tests and reporting issues --- README | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/README b/README index a41ad109..d0e842d1 100644 --- a/README +++ b/README @@ -63,7 +63,17 @@ acts_as_audited works with Rails 2.1 or later. == Contributing Contributions are always welcome. Checkout the latest code on GitHub: - http://github.com/collectiveidea/acts_as_audited + http://github.com/collectiveidea/acts_as_audited -Please report bugs or feature suggestions at: - http://collectiveidea.lighthouseapp.com/projects/20257-acts_as_audited +Please include tests with your patches. There are a few gems required to run the tests: + $ gem install multi_rails + $ gem install thoughtbot-should jeremymcanally-matchy --source http://gems.github.com + +Make sure the tests pass against all versions of Rails since 2.1: + + $ rake test:multi_rails:all + + + +Please report bugs or feature suggestions on GitHub: + http://github.com/collectiveidea/acts_as_audited/issues From 3a402526a3229666825d26ccf93868ad0476dfd8 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Sat, 23 May 2009 22:28:00 -0400 Subject: [PATCH 06/12] Attacking whitespace --- README | 2 -- 1 file changed, 2 deletions(-) diff --git a/README b/README index d0e842d1..052b2577 100644 --- a/README +++ b/README @@ -73,7 +73,5 @@ Make sure the tests pass against all versions of Rails since 2.1: $ rake test:multi_rails:all - - Please report bugs or feature suggestions on GitHub: http://github.com/collectiveidea/acts_as_audited/issues From 21fd3ac9719830bb1850bd5557df23a6e0926f00 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Sat, 23 May 2009 22:31:35 -0400 Subject: [PATCH 07/12] Fix typo in README --- README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README b/README index 052b2577..9157c211 100644 --- a/README +++ b/README @@ -67,7 +67,7 @@ Contributions are always welcome. Checkout the latest code on GitHub: Please include tests with your patches. There are a few gems required to run the tests: $ gem install multi_rails - $ gem install thoughtbot-should jeremymcanally-matchy --source http://gems.github.com + $ gem install thoughtbot-shoulda jeremymcanally-matchy --source http://gems.github.com Make sure the tests pass against all versions of Rails since 2.1: From b624ba06b7d39328be58a4ee80fb94a600e8b010 Mon Sep 17 00:00:00 2001 From: Kenneth Kalmer Date: Wed, 3 Jun 2009 14:19:54 +0200 Subject: [PATCH 08/12] Support for thread-safe 'background auditing' --- README | 6 +++ lib/acts_as_audited.rb | 77 ++++++++++++++++++++---------------- lib/acts_as_audited/audit.rb | 41 +++++++++++++------ test/acts_as_audited_test.rb | 40 ++++++++++++++++--- test/audit_test.rb | 72 ++++++++++++++++++++++++++++----- test/db/database.yml | 4 +- 6 files changed, 177 insertions(+), 63 deletions(-) diff --git a/README b/README index 9157c211..976ee08d 100644 --- a/README +++ b/README @@ -37,6 +37,12 @@ To get auditing outside of Rails you can explicitly declare acts_as_audited< acts_as_audited :except => [:password, :mistress] end +To record a user in the audits when the sweepers are not available, you can use as_user: + + Audit.as_user( user ) do + # Perform changes on audited models + end + See http://opensoul.org/2006/07/21/acts_as_audited for more information. == Caveats diff --git a/lib/acts_as_audited.rb b/lib/acts_as_audited.rb index 949c46d0..64d8c7db 100644 --- a/lib/acts_as_audited.rb +++ b/lib/acts_as_audited.rb @@ -1,5 +1,5 @@ # Copyright (c) 2006 Brandon Keepers -# +# # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including @@ -7,10 +7,10 @@ # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: -# +# # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -# +# # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND @@ -43,8 +43,8 @@ module ClassMethods # # * +only+ - Only audit the given attributes # * +except+ - Excludes fields from being saved in the audit log. - # By default, acts_as_audited will audit all but these fields: - # + # By default, acts_as_audited will audit all but these fields: + # # [self.primary_key, inheritance_column, 'lock_version', 'created_at', 'updated_at'] # You can add to those by passing one or an array of fields to skip. # @@ -60,16 +60,16 @@ module ClassMethods # acts_as_audited :protect => false # attr_accessible :name # end - # + # def acts_as_audited(options = {}) # don't allow multiple calls return if self.included_modules.include?(CollectiveIdea::Acts::Audited::InstanceMethods) - + options = {:protect => accessible_attributes.nil?}.merge(options) class_inheritable_reader :non_audited_columns class_inheritable_reader :auditing_enabled - + if options[:only] except = self.column_names - options[:only].flatten.map(&:to_s) else @@ -81,27 +81,27 @@ def acts_as_audited(options = {}) has_many :audits, :as => :auditable, :order => "#{Audit.quoted_table_name}.version" attr_protected :audit_ids if options[:protect] Audit.audited_class_names << self.to_s - + after_create :audit_create_callback before_update :audit_update_callback after_destroy :audit_destroy_callback - + attr_accessor :version extend CollectiveIdea::Acts::Audited::SingletonMethods include CollectiveIdea::Acts::Audited::InstanceMethods - + write_inheritable_attribute :auditing_enabled, true end end - + module InstanceMethods - + # Temporarily turns off auditing while saving. def save_without_auditing without_auditing { save } end - + # Executes the block with the auditing callbacks disabled. # # @foo.without_auditing do @@ -111,7 +111,7 @@ def save_without_auditing def without_auditing(&block) self.class.without_auditing(&block) end - + # Gets an array of the revisions available # # user.revisions.each do |revision| @@ -125,28 +125,28 @@ def revisions(from_version = 1) revision = self.audits.find_by_version(from_version).revision Audit.reconstruct_attributes(audits) {|attrs| revision.revision_with(attrs) } end - + # Get a specific revision specified by the version number, or +:previous+ def revision(version) revision_with Audit.reconstruct_attributes(audits_to(version)) end - + def revision_at(date_or_time) audits = self.audits.find(:all, :conditions => ["created_at <= ?", date_or_time]) revision_with Audit.reconstruct_attributes(audits) unless audits.empty? end - + def audited_attributes attributes.except(*non_audited_columns) end - + protected - + def revision_with(attributes) returning self.dup do |revision| revision.send :instance_variable_set, '@attributes', self.attributes_before_type_cast revision.attributes = attributes.reject {|attr,_| !respond_to?("#{attr}=") } - + # Remove any association proxies so that they will be recreated # and reference the correct object for this revision. The only way # to determine if an instance variable is a proxy object is to @@ -160,16 +160,16 @@ def revision_with(attributes) end end end - + private - + def audited_changes changed_attributes.except(*non_audited_columns).inject({}) do |changes,(attr, old_value)| changes[attr] = [old_value, self[attr]] changes end end - + def audits_to(version = nil) if version == :previous version = if self.version @@ -182,7 +182,7 @@ def audits_to(version = nil) end audits.find(:all, :conditions => ['version <= ?', version]) end - + def audit_create(user = nil) write_audit(:action => 'create', :changes => audited_attributes, :user => user) end @@ -196,20 +196,20 @@ def audit_update(user = nil) def audit_destroy(user = nil) write_audit(:action => 'destroy', :user => user, :changes => audited_attributes) end - + def write_audit(attrs) self.audits.create attrs if auditing_enabled end - CALLBACKS.each do |attr_name| + CALLBACKS.each do |attr_name| alias_method "#{attr_name}_callback".to_sym, attr_name end - + def empty_callback #:nodoc: end end # InstanceMethods - + module SingletonMethods # Returns an array of columns that are audited. See non_audited_columns def audited_columns @@ -227,15 +227,15 @@ def without_auditing(&block) disable_auditing returning(block.call) { enable_auditing if auditing_was_enabled } end - + def disable_auditing write_inheritable_attribute :auditing_enabled, false end - + def enable_auditing write_inheritable_attribute :auditing_enabled, true end - + def disable_auditing_callbacks class_eval do CALLBACKS.each do |attr_name| @@ -243,15 +243,22 @@ def disable_auditing_callbacks end end end - + def enable_auditing_callbacks - class_eval do + class_eval do CALLBACKS.each do |attr_name| alias_method "#{attr_name}_callback".to_sym, attr_name end end end - + + # All audit operations during the block are recorded as being + # made by +user+. This is not model specific, the method is a + # convenience wrapper around #Audit.as_user. + def audit_as( user, &block ) + Audit.as_user( user, &block ) + end + end end end diff --git a/lib/acts_as_audited/audit.rb b/lib/acts_as_audited/audit.rb index 5be18daa..b3a026ef 100644 --- a/lib/acts_as_audited/audit.rb +++ b/lib/acts_as_audited/audit.rb @@ -11,18 +11,32 @@ class Audit < ActiveRecord::Base belongs_to :auditable, :polymorphic => true belongs_to :user, :polymorphic => true - - before_create :set_version_number - + + before_create :set_version_number, :set_audit_user + serialize :changes - + cattr_accessor :audited_class_names self.audited_class_names = Set.new def self.audited_classes self.audited_class_names.map(&:constantize) end - + + cattr_accessor :audit_as_user + self.audit_as_user = nil + + # All audits made during the block called will be recorded as made + # by +user+. This method is hopefully threadsafe, making it ideal + # for background operations that require audit information. + def self.as_user(user, &block) + Thread.current[:acts_as_audited_user] = user + + yield + + Thread.current[:acts_as_audited_user] = nil + end + # Allows user to be set to either a string or an ActiveRecord object def user_as_string=(user) #:nodoc: # reset both either way @@ -39,7 +53,7 @@ def user_as_string #:nodoc: end alias_method :user_as_model, :user alias_method :user, :user_as_string - + def revision attributes = self.class.reconstruct_attributes(ancestors).merge({:version => version}) clazz = auditable_type.constantize @@ -47,13 +61,13 @@ def revision m.attributes = attributes end end - + def ancestors self.class.find(:all, :order => 'version', :conditions => ['auditable_id = ? and auditable_type = ? and version <= ?', auditable_id, auditable_type, version]) end - + # Returns a hash of the changed attributes with the new values def new_attributes (changes || {}).inject({}.with_indifferent_access) do |attrs,(attr,values)| @@ -69,7 +83,7 @@ def old_attributes attrs end end - + def self.reconstruct_attributes(audits) attributes = {} result = audits.collect do |audit| @@ -78,7 +92,7 @@ def self.reconstruct_attributes(audits) end block_given? ? result : attributes end - + private def set_version_number @@ -89,5 +103,10 @@ def set_version_number }) || 0 self.version = max + 1 end - + + def set_audit_user + self.user = Thread.current[:acts_as_audited_user] if Thread.current[:acts_as_audited_user] + nil # prevent stopping callback chains + end + end diff --git a/test/acts_as_audited_test.rb b/test/acts_as_audited_test.rb index d41a215c..0c2be425 100644 --- a/test/acts_as_audited_test.rb +++ b/test/acts_as_audited_test.rb @@ -1,7 +1,7 @@ require File.expand_path(File.dirname(__FILE__) + '/test_helper') module CollectiveIdea - module Acts + module Acts class AuditedTest < Test::Unit::TestCase should "include instance methods" do User.new.should be_kind_of(CollectiveIdea::Acts::Audited::InstanceMethods) @@ -16,7 +16,7 @@ class AuditedTest < Test::Unit::TestCase User.non_audited_columns.should include(column) end end - + should "not save non-audited columns" do create_user.audits.first.changes.keys.any?{|col| ['created_at', 'updated_at', 'password'].include? col}.should be(false) end @@ -307,14 +307,44 @@ class AccessibleUser < ActiveRecord::Base set_table_name :users attr_accessible :name, :username, :password # declare attr_accessible before calling aaa acts_as_audited - end + end should "not raise an error when attr_accessible is declared before acts_as_audited" do lambda{ AccessibleUser.new(:name => 'NO FAIL!') }.should_not raise_error end end - + + context "audit as" do + setup do + @user = User.create :name => 'Testing' + end + + should "record user objects" do + Company.audit_as( @user ) do + company = Company.create :name => 'The auditors' + company.name = 'The Auditors' + company.save + + company.audits.each do |audit| + audit.user.should == @user + end + end + end + + should "record usernames" do + Company.audit_as( @user.name ) do + company = Company.create :name => 'The auditors' + company.name = 'The Auditors, Inc' + company.save + + company.audits.each do |audit| + audit.username.should == @user.name + end + end + end + end + end end -end \ No newline at end of file +end diff --git a/test/audit_test.rb b/test/audit_test.rb index bc5bd5fd..a41af23b 100644 --- a/test/audit_test.rb +++ b/test/audit_test.rb @@ -11,7 +11,7 @@ def setup @audit.user = @user @audit.user.should == @user end - + should "be able to set the user to nil" do @audit.user_id = 1 @audit.user_type = 'User' @@ -24,7 +24,7 @@ def setup @audit.user_type.should == nil @audit.username.should == nil end - + should "be able to set the user to a string" do @audit.user = 'testing' @audit.user.should == 'testing' @@ -60,7 +60,7 @@ def setup revision.name.should == user.name revision.new_record?.should be(true) end - + should "set the version number on create" do user = User.create! :name => "Set Version Number" user.audits.first.version.should == 1 @@ -70,30 +70,30 @@ def setup user.destroy user.audits(true).last.version.should == 3 end - + context "reconstruct_attributes" do should "work with with old way of storing just the new value" do audits = Audit.reconstruct_attributes([Audit.new(:changes => {'attribute' => 'value'})]) audits['attribute'].should == 'value' end end - + context "audited_classes" do class CustomUser < ActiveRecord::Base end class CustomUserSubclass < CustomUser acts_as_audited end - + should "include audited classes" do Audit.audited_classes.should include(User) end - + should "include subclasses" do Audit.audited_classes.should include(CustomUserSubclass) end end - + context "new_attributes" do should "return a hash of the new values" do Audit.new(:changes => {:a => [1, 2], :b => [3, 4]}).new_attributes.should == {'a' => 2, 'b' => 4} @@ -105,6 +105,58 @@ class CustomUserSubclass < CustomUser Audit.new(:changes => {:a => [1, 2], :b => [3, 4]}).old_attributes.should == {'a' => 1, 'b' => 3} end end - -end \ No newline at end of file + context "as_user" do + setup do + @user = User.create :name => 'testing' + end + + should "record user objects" do + Audit.as_user(@user) do + company = Company.create :name => 'The auditors' + company.name = 'The Auditors, Inc' + company.save + + company.audits.each do |audit| + audit.user.should == @user + end + end + end + + should "record usernames" do + Audit.as_user(@user.name) do + company = Company.create :name => 'The auditors' + company.name = 'The Auditors, Inc' + company.save + + company.audits.each do |audit| + audit.username.should == @user.name + end + end + end + + should "be thread safe" do + begin + t1 = Thread.new do + Audit.as_user(@user) do + sleep 1 + Company.create(:name => 'The Auditors, Inc').audits.first.user.should == @user + end + end + + t2 = Thread.new do + Audit.as_user(@user.name) do + Company.create(:name => 'The Competing Auditors, LLC').audits.first.username.should == @user.name + sleep 0.5 + end + end + + t1.join + t2.join + rescue ActiveRecord::StatementInvalid + STDERR.puts "Thread safety tests cannot be run with SQLite" + end + end + end + +end diff --git a/test/db/database.yml b/test/db/database.yml index 70d92c11..63317ff2 100644 --- a/test/db/database.yml +++ b/test/db/database.yml @@ -16,6 +16,6 @@ postgresql: mysql: :adapter: mysql :host: localhost - :username: rails + :username: root :password: - :database: acts_as_audited_plugin_test \ No newline at end of file + :database: acts_as_audited_plugin_test From e4adb6bc7e40249b9e2b91cf609031e6fd1f36fb Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Wed, 3 Jun 2009 09:20:44 -0400 Subject: [PATCH 09/12] Require jnunemaker's fork of matchy --- README | 2 +- test/test_helper.rb | 97 +++------------------------------------------ 2 files changed, 6 insertions(+), 93 deletions(-) diff --git a/README b/README index 976ee08d..221cf761 100644 --- a/README +++ b/README @@ -73,7 +73,7 @@ Contributions are always welcome. Checkout the latest code on GitHub: Please include tests with your patches. There are a few gems required to run the tests: $ gem install multi_rails - $ gem install thoughtbot-shoulda jeremymcanally-matchy --source http://gems.github.com + $ gem install thoughtbot-shoulda jnunemaker-matchy --source http://gems.github.com Make sure the tests pass against all versions of Rails since 2.1: diff --git a/test/test_helper.rb b/test/test_helper.rb index fd770ad9..89ea05b9 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -10,6 +10,8 @@ require 'action_view' require 'test/unit' require 'shoulda' + +gem 'jnunemaker-matchy' require 'matchy' require File.dirname(__FILE__) + '/../init.rb' @@ -26,9 +28,9 @@ class Company < ActiveRecord::Base end class Test::Unit::TestCase - def change(receiver=nil, message=nil, &block) - ChangeExpectation.new(self, receiver, message, &block) - end + # def change(receiver=nil, message=nil, &block) + # ChangeExpectation.new(self, receiver, message, &block) + # end def create_user(attrs = {}) User.create({:name => 'Brandon', :username => 'brandon', :password => 'password'}.merge(attrs)) @@ -43,92 +45,3 @@ def create_versions(n = 2) end end end - -# Change matcher copied from RSpec -class ChangeExpectation < Matchy::Expectations::Base - def initialize(test_case, receiver=nil, message=nil, &block) - @test_case = test_case - @message = message || "result" - @value_proc = block || lambda {receiver.__send__(message)} - @to = @from = @minimum = @maximum = @amount = nil - end - - def matches?(event_proc) - raise_block_syntax_error if block_given? - - @before = evaluate_value_proc - event_proc.call - @after = evaluate_value_proc - - return (@to = false) if @from unless @from == @before - return false if @to unless @to == @after - return (@before + @amount == @after) if @amount - return ((@after - @before) >= @minimum) if @minimum - return ((@after - @before) <= @maximum) if @maximum - return @before != @after - end - - def raise_block_syntax_error - raise(<<-MESSAGE -block passed to should or should_not change must use {} instead of do/end -MESSAGE - ) - end - - def evaluate_value_proc - @value_proc.call - end - - def failure_message - if @to - "#{@message} should have been changed to #{@to.inspect}, but is now #{@after.inspect}" - elsif @from - "#{@message} should have initially been #{@from.inspect}, but was #{@before.inspect}" - elsif @amount - "#{@message} should have been changed by #{@amount.inspect}, but was changed by #{actual_delta.inspect}" - elsif @minimum - "#{@message} should have been changed by at least #{@minimum.inspect}, but was changed by #{actual_delta.inspect}" - elsif @maximum - "#{@message} should have been changed by at most #{@maximum.inspect}, but was changed by #{actual_delta.inspect}" - else - "#{@message} should have changed, but is still #{@before.inspect}" - end - end - - def actual_delta - @after - @before - end - - def negative_failure_message - "#{@message} should not have changed, but did change from #{@before.inspect} to #{@after.inspect}" - end - - def by(amount) - @amount = amount - self - end - - def by_at_least(minimum) - @minimum = minimum - self - end - - def by_at_most(maximum) - @maximum = maximum - self - end - - def to(to) - @to = to - self - end - - def from (from) - @from = from - self - end - - def description - "change ##{@message}" - end -end From 58f24e396d3f12b14e10bbb71c07af20db47c752 Mon Sep 17 00:00:00 2001 From: Lance Ivy Date: Fri, 31 Jul 2009 17:32:31 -0700 Subject: [PATCH 10/12] when recreating revisions, update protected attributes and bypass special attribute assignment methods --- lib/acts_as_audited.rb | 8 +++++++- test/acts_as_audited_test.rb | 15 +++++++++++++++ test/test_helper.rb | 6 ++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/acts_as_audited.rb b/lib/acts_as_audited.rb index 64d8c7db..3f05516c 100644 --- a/lib/acts_as_audited.rb +++ b/lib/acts_as_audited.rb @@ -145,7 +145,13 @@ def audited_attributes def revision_with(attributes) returning self.dup do |revision| revision.send :instance_variable_set, '@attributes', self.attributes_before_type_cast - revision.attributes = attributes.reject {|attr,_| !respond_to?("#{attr}=") } + attributes.each do |attr, val| + if respond_to?("#{attr}=") + self.attributes.has_key?(attr.to_s) ? + revision[attr] = val : + revision.send("#{attr}=", val) + end + end # Remove any association proxies so that they will be recreated # and reference the correct object for this revision. The only way diff --git a/test/acts_as_audited_test.rb b/test/acts_as_audited_test.rb index 0c2be425..690dc75c 100644 --- a/test/acts_as_audited_test.rb +++ b/test/acts_as_audited_test.rb @@ -221,6 +221,21 @@ class AuditedTest < Test::Unit::TestCase previous.version.should == 4 previous.revision(:previous).version.should == 3 end + + should "be able to set protected attributes" do + u = User.create(:name => 'Brandon') + u.update_attribute :logins, 1 + u.update_attribute :logins, 2 + + u.revision(3).logins.should == 2 + u.revision(2).logins.should == 1 + u.revision(1).logins.should == 0 + end + + should "set attributes directly" do + u = User.create(:name => '') + u.revision(1).name.should == '<Joe>' + end should "set the attributes for each revision" do u = User.create(:name => 'Brandon', :username => 'brandon') diff --git a/test/test_helper.rb b/test/test_helper.rb index 89ea05b9..7827d3d7 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -23,6 +23,12 @@ class User < ActiveRecord::Base acts_as_audited :except => :password + + attr_protected :logins + + def name=(val) + write_attribute(:name, CGI.escapeHTML(val)) + end end class Company < ActiveRecord::Base end From 4fe992c0343ad47182161012ba099c1b4e9d0bb3 Mon Sep 17 00:00:00 2001 From: Lance Ivy Date: Wed, 5 Aug 2009 10:44:19 -0700 Subject: [PATCH 11/12] use same attribute assignment for Audit#revision as for Klass#revision_with --- lib/acts_as_audited.rb | 8 +----- lib/acts_as_audited/audit.rb | 14 +++++++++-- test/audit_test.rb | 47 ++++++++++++++++++++++++------------ 3 files changed, 45 insertions(+), 24 deletions(-) diff --git a/lib/acts_as_audited.rb b/lib/acts_as_audited.rb index 3f05516c..dd517a46 100644 --- a/lib/acts_as_audited.rb +++ b/lib/acts_as_audited.rb @@ -145,13 +145,7 @@ def audited_attributes def revision_with(attributes) returning self.dup do |revision| revision.send :instance_variable_set, '@attributes', self.attributes_before_type_cast - attributes.each do |attr, val| - if respond_to?("#{attr}=") - self.attributes.has_key?(attr.to_s) ? - revision[attr] = val : - revision.send("#{attr}=", val) - end - end + Audit.assign_revision_attributes(revision, attributes) # Remove any association proxies so that they will be recreated # and reference the correct object for this revision. The only way diff --git a/lib/acts_as_audited/audit.rb b/lib/acts_as_audited/audit.rb index b3a026ef..50106b9f 100644 --- a/lib/acts_as_audited/audit.rb +++ b/lib/acts_as_audited/audit.rb @@ -55,10 +55,9 @@ def user_as_string #:nodoc: alias_method :user, :user_as_string def revision - attributes = self.class.reconstruct_attributes(ancestors).merge({:version => version}) clazz = auditable_type.constantize returning clazz.find_by_id(auditable_id) || clazz.new do |m| - m.attributes = attributes + Audit.assign_revision_attributes(m, self.class.reconstruct_attributes(ancestors).merge({:version => version})) end end @@ -92,6 +91,17 @@ def self.reconstruct_attributes(audits) end block_given? ? result : attributes end + + def self.assign_revision_attributes(record, attributes) + attributes.each do |attr, val| + if record.respond_to?("#{attr}=") + record.attributes.has_key?(attr.to_s) ? + record[attr] = val : + record.send("#{attr}=", val) + end + end + record + end private diff --git a/test/audit_test.rb b/test/audit_test.rb index a41af23b..ad5090f1 100644 --- a/test/audit_test.rb +++ b/test/audit_test.rb @@ -44,23 +44,40 @@ def setup end end - - should "revision" do - user = User.create :name => "1" - 5.times {|i| user.update_attribute :name, (i + 2).to_s } - user.audits.each do |audit| - audit.revision.name.should == audit.version.to_s + + context "revision" do + should "recreate attributes" do + user = User.create :name => "1" + 5.times {|i| user.update_attribute :name, (i + 2).to_s } + user.audits.each do |audit| + audit.revision.name.should == audit.version.to_s + end + end + + should "set protected attributes" do + u = User.create(:name => 'Brandon') + u.update_attribute :logins, 1 + u.update_attribute :logins, 2 + + u.audits[2].revision.logins.should == 2 + u.audits[1].revision.logins.should == 1 + u.audits[0].revision.logins.should == 0 + end + + should "bypass attribute assignment wrappers" do + u = User.create(:name => '') + u.audits.first.revision.name.should == '<Joe>' + end + + should "work for deleted records" do + user = User.create :name => "1" + user.destroy + revision = user.audits.last.revision + revision.name.should == user.name + revision.new_record?.should be(true) end end - - should "be able to create revision for deleted records" do - user = User.create :name => "1" - user.destroy - revision = user.audits.last.revision - revision.name.should == user.name - revision.new_record?.should be(true) - end - + should "set the version number on create" do user = User.create! :name => "Set Version Number" user.audits.first.version.should == 1 From 02e35ac368f3125887c4752a39610f5e914ff6b7 Mon Sep 17 00:00:00 2001 From: Kenneth Kalmer Date: Sat, 8 Aug 2009 16:21:42 +0200 Subject: [PATCH 12/12] Ruby 1.9 compat (fixes #7) --- lib/acts_as_audited/audit_sweeper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/acts_as_audited/audit_sweeper.rb b/lib/acts_as_audited/audit_sweeper.rb index d9d58914..28ba24c9 100644 --- a/lib/acts_as_audited/audit_sweeper.rb +++ b/lib/acts_as_audited/audit_sweeper.rb @@ -37,7 +37,7 @@ def audit(*models) models << [key, options.delete(key)] if key.is_a?(Class) end - models.each do |model, model_options| + models.each do |(model, model_options)| model.send :acts_as_audited, model_options || {} # disable ActiveRecord callbacks, which are replaced by the AuditSweeper