auditor 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,15 @@
1
1
  module Auditor
2
2
  module User
3
+
3
4
  def current_user
4
- Thread.current[@@current_user_symbol]
5
+ Thread.current[:auditor_user]
5
6
  end
6
7
 
7
8
  def current_user=(user)
8
- Thread.current[@@current_user_symbol] = user
9
+ Thread.current[:auditor_user] = user
9
10
  end
10
-
11
+
11
12
  module_function :current_user, :current_user=
12
13
 
13
- private
14
- @@current_user_symbol = :auditor_current_user
15
14
  end
16
- end
15
+ end
@@ -1,3 +1,3 @@
1
1
  module Auditor
2
- VERSION = "1.0.0"
3
- end
2
+ VERSION = "2.0.0"
3
+ end
@@ -5,11 +5,11 @@ module Auditor
5
5
  module Generators
6
6
  class MigrationGenerator < Rails::Generators::Base
7
7
  include Rails::Generators::Migration
8
-
8
+
9
9
  desc "Create migration for Auditor audits table"
10
-
10
+
11
11
  source_root File.expand_path("../templates", __FILE__)
12
-
12
+
13
13
  def self.next_migration_number(dirname)
14
14
  if ActiveRecord::Base.timestamped_migrations
15
15
  Time.now.utc.strftime("%Y%m%d%H%M%S")
@@ -23,4 +23,4 @@ module Auditor
23
23
  end
24
24
  end
25
25
  end
26
- end
26
+ end
@@ -3,17 +3,21 @@ class CreateAuditsTable < ActiveRecord::Migration
3
3
  create_table :audits, :force => true do |t|
4
4
  t.column :auditable_id, :integer, :null => false
5
5
  t.column :auditable_type, :string, :null => false
6
- t.column :auditable_version, :integer
7
6
  t.column :user_id, :integer, :null => false
8
7
  t.column :user_type, :string, :null => false
9
8
  t.column :action, :string, :null => false
10
- t.column :message, :text
11
- t.column :edits, :text
9
+ t.column :audited_changes, :text
10
+ t.column :version, :integer, :default => 0
11
+ t.column :comment, :text
12
12
  t.column :created_at, :datetime, :null => false
13
13
  end
14
+
15
+ add_index :audits, [:auditable_id, :auditable_type], :name => 'auditable_index'
16
+ add_index :audits, [:user_id, :user_type], :name => 'user_index'
17
+ add_index :audits, :created_at
14
18
  end
15
19
 
16
20
  def self.down
17
21
  drop_table :audits
18
22
  end
19
- end
23
+ end
@@ -0,0 +1,63 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require 'auditor/audit'
3
+
4
+ describe Audit do
5
+ before(:each) do
6
+ @auditable = Model.create
7
+ @user = User.create
8
+ end
9
+
10
+ it 'should set the version number on save' do
11
+ audit = Audit.create(:auditable => @auditable, :user => @user, :action => :create)
12
+ audit.version.should == 1
13
+ end
14
+
15
+ it 'should provide access to the audited model object' do
16
+ audit = Audit.create(:auditable => @auditable, :user => @user, :action => :create)
17
+ audit.auditable.should == @auditable
18
+ end
19
+ it 'should provide access to the user associated with the audit' do
20
+ audit = Audit.create(:auditable => @auditable, :user => @user, :action => :create)
21
+ audit.user.should == @user
22
+ end
23
+
24
+ it 'should create a snapshot of the audited objects attributes at the time of the audit' do
25
+ audit1 = Audit.create(:auditable => @auditable, :user => @user, :action => :create)
26
+ audit2 = Audit.create(:auditable => @auditable, :user => @user, :action => :update, :audited_changes => {'name' => [nil, 'n1'], 'value' => [nil, 'v1']})
27
+ audit3 = Audit.create(:auditable => @auditable, :user => @user, :action => :find)
28
+ audit4 = Audit.create(:auditable => @auditable, :user => @user, :action => :update, :audited_changes => {'value' => [nil, 'v2']})
29
+
30
+ audit1.attribute_snapshot.should == {}
31
+ audit2.attribute_snapshot.should == {'name' => 'n1', 'value' => 'v1'}
32
+ audit3.attribute_snapshot.should == {'name' => 'n1', 'value' => 'v1'}
33
+ audit4.attribute_snapshot.should == {'name' => 'n1', 'value' => 'v2'}
34
+ end
35
+
36
+ describe 'modifying scope' do
37
+ it 'should return all audit records that were a result of modifying the audited object attributes' do
38
+ audit1 = Audit.create(:auditable => @auditable, :user => @user, :action => :create)
39
+ audit2 = Audit.create(:auditable => @auditable, :user => @user, :action => :update, :audited_changes => {'name' => [nil, 'n1'], 'value' => [nil, 'v1']})
40
+ audit3 = Audit.create(:auditable => @auditable, :user => @user, :action => :find)
41
+ audit4 = Audit.create(:auditable => @auditable, :user => @user, :action => :update, :audited_changes => {'value' => [nil, 'v2']})
42
+
43
+ Audit.modifying.should include(audit1, audit2, audit4)
44
+ Audit.modifying.should_not include(audit3)
45
+ end
46
+ end
47
+
48
+ describe 'predecessors scope' do
49
+ it 'should return all previous audit records for the same auditable' do
50
+ auditable2 = Model.create
51
+ audit1 = Audit.create(:auditable => @auditable, :user => @user, :action => :create)
52
+ audit2 = Audit.create(:auditable => @auditable, :user => @user, :action => :update, :audited_changes => {'name' => [nil, 'n1'], 'value' => [nil, 'v1']})
53
+ audit3 = Audit.create(:auditable => auditable2, :user => @user, :action => :find)
54
+ audit4 = Audit.create(:auditable => @auditable, :user => @user, :action => :update, :audited_changes => {'value' => [nil, 'v2']})
55
+
56
+ Audit.trail(audit4).should include(audit1, audit2, audit4)
57
+ Audit.trail(audit4).should_not include(audit3)
58
+ end
59
+ end
60
+ end
61
+
62
+ class Model < ActiveRecord::Base; end
63
+ class User < ActiveRecord::Base; end
@@ -0,0 +1,111 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require 'auditor/user'
3
+ require 'auditor/status'
4
+
5
+ describe Auditor::Auditable do
6
+ include Auditor::Status
7
+
8
+ before(:each) do
9
+ @user = User.create
10
+ Auditor::User.current_user = @user
11
+ end
12
+
13
+ it 'should audit find' do
14
+ redefine_model { audit!(:find) }
15
+ m = without_auditing { Model.create }
16
+
17
+ Model.find(m.id)
18
+
19
+ verify_audit(Audit.last, m)
20
+ end
21
+
22
+ it 'should audit create' do
23
+ redefine_model { audit!(:create) }
24
+
25
+ m = Model.create(:name => 'new')
26
+
27
+ verify_audit(Audit.last, m, { 'name' => [nil, 'new'], 'id' => [nil, m.id] })
28
+ end
29
+
30
+ it 'should audit update' do
31
+ redefine_model { audit!(:update) }
32
+ m = without_auditing { Model.create(:name => 'new') }
33
+
34
+ m.update_attributes(:name => 'newer')
35
+
36
+ verify_audit(Audit.last, m, { 'name' => ['new', 'newer'] })
37
+ end
38
+
39
+ it 'should audit destroy' do
40
+ redefine_model { audit!(:destroy) }
41
+ m = without_auditing { Model.create }
42
+
43
+ m.destroy
44
+
45
+ verify_audit(Audit.last, m)
46
+ end
47
+
48
+ it 'should allow multiple actions to be specified with one audit statment' do
49
+ redefine_model { audit!(:create, :destroy) }
50
+
51
+ m = Model.create
52
+ m.reload
53
+ m = Model.find(m.id)
54
+ m.update_attributes({:name => 'new'})
55
+ m.destroy
56
+
57
+ Audit.count.should == 2
58
+ end
59
+
60
+ it 'should record the comment returned from a comment block' do
61
+ redefine_model { audit!(:create) { 'comment' } }
62
+ Model.create
63
+ Audit.last.comment.should == 'comment'
64
+ end
65
+
66
+ it 'should provide the model object and user as parameters to the comment block' do
67
+ id = without_auditing { Model.create }.id
68
+ user = @user
69
+ redefine_model {
70
+ audit!(:find) { |model, user|
71
+ model.id.should == id
72
+ user.should == user
73
+ }
74
+ }
75
+ Model.find(id)
76
+ end
77
+
78
+ it 'should provide a snapshot of the object attributes at a given date or time' do
79
+ redefine_model { audit!(:create, :find, :update, :destroy) }
80
+ m = Model.create(:name => '1')
81
+ ts1 = Time.now
82
+ m = Model.find(m.id)
83
+ ts2 = Time.now
84
+ m.update_attributes(:name => '2')
85
+ ts3 = Time.now
86
+ m.destroy
87
+ ts4 = Time.now
88
+
89
+ m.attributes_at(ts1).should == {'name' => '1', 'id' => m.id}
90
+ m.attributes_at(ts2).should == {'name' => '1', 'id' => m.id}
91
+ m.attributes_at(ts3).should == {'name' => '2', 'id' => m.id}
92
+ m.attributes_at(ts4).should == {'name' => '2', 'id' => m.id}
93
+ end
94
+
95
+ def verify_audit(audit, model, changes=nil)
96
+ audit.should_not be_nil
97
+ audit.auditable.should == model unless audit.action == 'destroy'
98
+ audit.user.should == @user
99
+ audit.audited_changes.should == changes unless changes.nil?
100
+ end
101
+
102
+ def redefine_model(&blk)
103
+ clazz = Class.new(ActiveRecord::Base, &blk)
104
+ Object.send :remove_const, 'Model'
105
+ Object.send :const_set, 'Model', clazz
106
+ end
107
+
108
+ class ::User < ActiveRecord::Base; end
109
+ class ::Model < ActiveRecord::Base; end
110
+
111
+ end
@@ -0,0 +1,49 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require 'auditor/config'
3
+
4
+ describe Auditor::Config do
5
+
6
+ describe 'Configuration' do
7
+ it "should parse actions and options from a config array" do
8
+ config = Auditor::Config.new(:create, 'update', {:only => :username})
9
+ config.actions.should_not be_nil
10
+ config.options.should_not be_nil
11
+ config.actions.should have(2).items
12
+ config.actions.should =~ [:create, :update]
13
+ config.options.should == {:only => ["username"], :except => []}
14
+ end
15
+
16
+ it "should parse actions and options from a config array when options are absent" do
17
+ config = Auditor::Config.new(:create, 'update')
18
+ config.actions.should_not be_nil
19
+ config.actions.should have(2).items
20
+ config.actions.should =~ [:create, :update]
21
+ config.options.should == {:only => [], :except => []}
22
+ end
23
+
24
+ it "should parse actions" do
25
+ config = Auditor::Config.new(:create)
26
+ config.actions.should_not be_nil
27
+ config.actions.should have(1).item
28
+ config.actions.should =~ [:create]
29
+ config.options.should == {:only => [], :except => []}
30
+ end
31
+
32
+ end
33
+
34
+ describe 'Configuration Validation' do
35
+ it "should raise a Auditor::Error if no action is specified" do
36
+ lambda {
37
+ Auditor::Config.new
38
+ }.should raise_error(Auditor::Error)
39
+ end
40
+
41
+ it "should raise a Auditor::Error if an invalid action is specified" do
42
+ lambda {
43
+ Auditor::Config.new(:create, :view)
44
+ }.should raise_error(Auditor::Error)
45
+ end
46
+
47
+ end
48
+
49
+ end
@@ -1,120 +1,86 @@
1
1
  require File.dirname(__FILE__) + '/spec_helper'
2
- require 'auditor'
2
+ require 'auditor/user'
3
3
 
4
- describe Auditor::Recorder do
4
+ describe Auditor::Recorder do
5
5
  before(:each) do
6
- @user = Auditor::User.current_user = new_model(7)
6
+ @user = Auditor::User.current_user = User.create
7
7
  end
8
-
9
- it 'should create and save a new audit record' do
10
- model = new_model(42, {'first_name' => ['old','new']})
11
-
12
- auditor = Auditor::Recorder.new(:create, model, {:except => [], :only => []}) { |m, u| "Model: #{m.id} User: #{u.id}" }
13
-
14
- audit = do_callbacks(auditor, model)
15
-
16
- audit.saved.should be_true
17
- audit.action.should == 'create'
18
- audit.edits.to_a.should =~ [['first_name', ['old', 'new']]]
19
- audit.auditable_id.should == 42
8
+
9
+ it 'should create an audit record for create actions' do
10
+ verify_action(:create)
11
+ end
12
+
13
+ it 'should create an audit record for find actions' do
14
+ verify_action(:find)
15
+ end
16
+
17
+ it 'should create an audit record for update actions' do
18
+ verify_action(:update)
19
+ end
20
+
21
+ it 'should create an audit record for destroy actions' do
22
+ verify_action(:destroy)
23
+ end
24
+
25
+ def verify_action(action)
26
+ model = Model.create
27
+ model.reload
28
+ model.name = 'changed'
29
+ config = Auditor::Config.new(action)
30
+
31
+ recorder = Auditor::Recorder.new(config.options) { 'comment' }
32
+ recorder.send "after_#{action}", model
33
+ audit = Audit.last
34
+
35
+ audit.action.should == action.to_s
36
+ audit.auditable_id.should == model.id
20
37
  audit.auditable_type.should == model.class.to_s
21
38
  audit.user_id.should == @user.id
22
39
  audit.user_type.should == @user.class.to_s
23
- audit.auditable_version.should be_nil
24
- audit.message.should == 'Model: 42 User: 7'
25
- end
26
-
27
- it 'should capture the new id of a created record' do
28
- model = new_model
29
-
30
- auditor = Auditor::Recorder.new(:create, model, {:except => [], :only => []})
31
-
32
- audit = do_callbacks(auditor, model)
33
-
34
- audit.saved.should be_true
35
- audit.auditable_id.should == 42
36
- audit.auditable_type.should == model.class.to_s
40
+ audit.comment.should == 'comment'
41
+ audit.audited_changes.should == {'name' => [nil, 'changed'] } if [:create, :update].include?(action)
42
+
43
+ audit.user.should == @user
44
+ audit.auditable.should == model
37
45
  end
38
-
39
- it 'should set message details to nil if they are not given' do
40
- model = new_model
41
- auditor = Auditor::Recorder.new(:create, model, {:except => [], :only => []})
42
-
43
- audit = do_callbacks(auditor, model)
44
-
45
- audit.saved.should be_true
46
- audit.message.should be_nil
46
+
47
+ it 'should set comment details to nil if they are not given' do
48
+ model = Model.create
49
+ config = Auditor::Config.new(:create)
50
+
51
+ recorder = Auditor::Recorder.new(config.options)
52
+ recorder.after_create(model)
53
+ audit = Audit.last
54
+
55
+ audit.comment.should be_nil
47
56
  end
48
-
57
+
49
58
  it 'should not save change details for excepted attributes' do
50
- model = new_model(42, {'first_name' => ['old','new'], 'last_name' => ['old','new']})
51
-
52
- auditor = Auditor::Recorder.new(:create, model, {:except => ['last_name'], :only => []})
53
-
54
- audit = do_callbacks(auditor, model)
55
-
56
- audit.saved.should be_true
57
- audit.edits.to_a.should =~ [['first_name', ['old', 'new']]]
59
+ model = Model.create
60
+ model.name = 'changed'
61
+ model.value = 'newval'
62
+ config = Auditor::Config.new(:create, :except => :name)
63
+
64
+ recorder = Auditor::Recorder.new(config.options)
65
+ recorder.after_create(model)
66
+ audit = Audit.last
67
+
68
+ audit.audited_changes.should == {'value' => [nil, 'newval'] }
58
69
  end
59
-
70
+
60
71
  it 'should only save change details for onlyed attributes' do
61
- model = new_model(42, {'first_name' => ['old','new'], 'last_name' => ['old','new']})
62
-
63
- auditor = Auditor::Recorder.new(:create, model, {:except => [], :only => ['last_name']})
64
-
65
- audit = do_callbacks(auditor, model)
66
-
67
- audit.saved.should be_true
68
- audit.edits.to_a.should =~ [['last_name', ['old', 'new']]]
69
- end
70
-
71
- it 'should not save attributes listed in both the only and except options' do
72
- model = new_model(42, {'first_name' => ['old','new'], 'last_name' => ['old','new']})
73
-
74
- auditor = Auditor::Recorder.new(:create, model, {:except => ['last_name'], :only => ['last_name']})
75
-
76
- audit = do_callbacks(auditor, model)
77
-
78
- audit.saved.should be_true
79
- audit.edits.should be_nil
80
- end
81
-
82
- def do_callbacks(auditor, model)
83
- auditor.audit_before
84
- audit = auditor.instance_variable_get(:@audit)
85
- audit.saved.should be_false
86
-
87
- model.changes = nil
88
- model.id = 42 if model.id.nil?
89
-
90
- auditor.audit_after
91
- auditor.instance_variable_get(:@audit)
92
- end
93
-
94
- def new_model(id = nil, changes = {})
95
- model = (Class.new do; attr_accessor :id, :changes; end).new
96
- model.id, model.changes = id, changes
97
- model
98
- end
99
-
100
- class Audit
101
- attr_accessor :edits, :saved
102
- attr_accessor :action, :auditable_id, :auditable_type, :user_id, :user_type, :auditable_version, :message
103
-
104
- def initialize(attrs={})
105
- @edits = attrs.delete(:edits)
106
- @saved = false
107
- raise "You can only set the edits field in this class" unless attrs.empty?
108
- end
109
-
110
- def attributes=(attrs={})
111
- attrs.each_pair do |key, val|
112
- self.send("#{key}=".to_sym, val)
113
- end
114
- end
115
-
116
- def save
117
- @saved = true
118
- end
72
+ model = Model.create
73
+ model.name = 'changed'
74
+ model.value = 'newval'
75
+ config = Auditor::Config.new(:create, :only => :name)
76
+
77
+ recorder = Auditor::Recorder.new(config.options)
78
+ recorder.after_create(model)
79
+ audit = Audit.last
80
+
81
+ audit.audited_changes.should == {'name' => [nil, 'changed'] }
119
82
  end
120
- end
83
+
84
+ class Model < ActiveRecord::Base; end
85
+ class User < ActiveRecord::Base; end
86
+ end