audited 4.2.2 → 4.3.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of audited might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.travis.yml +10 -9
- data/Appraisals +10 -6
- data/Gemfile +1 -13
- data/README.md +46 -33
- data/Rakefile +3 -18
- data/gemfiles/rails40.gemfile +1 -5
- data/gemfiles/rails41.gemfile +1 -5
- data/gemfiles/rails42.gemfile +1 -5
- data/gemfiles/rails50.gemfile +8 -0
- data/lib/audited-rspec.rb +4 -0
- data/lib/audited.rb +15 -2
- data/lib/audited/audit.rb +97 -57
- data/lib/audited/auditor.rb +73 -45
- data/lib/audited/rspec_matchers.rb +6 -2
- data/lib/audited/sweeper.rb +12 -23
- data/lib/audited/version.rb +1 -1
- data/lib/generators/audited/install_generator.rb +20 -0
- data/lib/generators/audited/migration.rb +15 -0
- data/lib/generators/audited/templates/add_association_to_audits.rb +11 -0
- data/lib/generators/audited/templates/add_comment_to_audits.rb +9 -0
- data/lib/generators/audited/templates/add_remote_address_to_audits.rb +10 -0
- data/lib/generators/audited/templates/add_request_uuid_to_audits.rb +10 -0
- data/lib/generators/audited/templates/install.rb +30 -0
- data/lib/generators/audited/templates/rename_association_to_associated.rb +23 -0
- data/lib/generators/audited/templates/rename_changes_to_audited_changes.rb +9 -0
- data/lib/generators/audited/templates/rename_parent_to_association.rb +11 -0
- data/lib/generators/audited/upgrade_generator.rb +57 -0
- data/spec/audited/audit_spec.rb +199 -0
- data/spec/audited/auditor_spec.rb +607 -0
- data/spec/audited/sweeper_spec.rb +106 -0
- data/spec/audited_spec_helpers.rb +6 -22
- data/spec/rails_app/config/environments/test.rb +7 -4
- data/spec/rails_app/config/initializers/secret_token.rb +1 -1
- data/spec/rails_app/config/routes.rb +1 -4
- data/spec/spec_helper.rb +7 -9
- data/spec/support/active_record/models.rb +20 -13
- data/spec/support/active_record/schema.rb +36 -12
- data/test/db/version_1.rb +4 -4
- data/test/db/version_2.rb +4 -4
- data/test/db/version_3.rb +4 -4
- data/test/db/version_4.rb +4 -4
- data/test/db/version_5.rb +2 -2
- data/test/db/version_6.rb +2 -2
- data/test/install_generator_test.rb +1 -1
- data/test/upgrade_generator_test.rb +10 -10
- metadata +73 -37
- data/lib/audited/active_record/version.rb +0 -5
- data/lib/audited/mongo_mapper/version.rb +0 -5
- data/spec/support/mongo_mapper/connection.rb +0 -4
- data/spec/support/mongo_mapper/models.rb +0 -214
@@ -0,0 +1,23 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
if index_exists? :audits, [:association_id, :association_type], :name => 'association_index'
|
4
|
+
remove_index :audits, :name => 'association_index'
|
5
|
+
end
|
6
|
+
|
7
|
+
rename_column :audits, :association_id, :associated_id
|
8
|
+
rename_column :audits, :association_type, :associated_type
|
9
|
+
|
10
|
+
add_index :audits, [:associated_id, :associated_type], :name => 'associated_index'
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.down
|
14
|
+
if index_exists? :audits, [:associated_id, :associated_type], :name => 'associated_index'
|
15
|
+
remove_index :audits, :name => 'associated_index'
|
16
|
+
end
|
17
|
+
|
18
|
+
rename_column :audits, :associated_type, :association_type
|
19
|
+
rename_column :audits, :associated_id, :association_id
|
20
|
+
|
21
|
+
add_index :audits, [:association_id, :association_type], :name => 'association_index'
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
rename_column :audits, :auditable_parent_id, :association_id
|
4
|
+
rename_column :audits, :auditable_parent_type, :association_type
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.down
|
8
|
+
rename_column :audits, :association_type, :auditable_parent_type
|
9
|
+
rename_column :audits, :association_id, :auditable_parent_id
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
require 'active_record'
|
4
|
+
require 'rails/generators/active_record'
|
5
|
+
require 'generators/audited/migration'
|
6
|
+
|
7
|
+
module Audited
|
8
|
+
module Generators
|
9
|
+
class UpgradeGenerator < Rails::Generators::Base
|
10
|
+
include Rails::Generators::Migration
|
11
|
+
extend Audited::Generators::Migration
|
12
|
+
|
13
|
+
source_root File.expand_path("../templates", __FILE__)
|
14
|
+
|
15
|
+
def copy_templates
|
16
|
+
migrations_to_be_applied do |m|
|
17
|
+
migration_template "#{m}.rb", "db/migrate/#{m}.rb"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def migrations_to_be_applied
|
24
|
+
Audited::Audit.reset_column_information
|
25
|
+
columns = Audited::Audit.columns.map(&:name)
|
26
|
+
|
27
|
+
yield :add_comment_to_audits unless columns.include?('comment')
|
28
|
+
|
29
|
+
if columns.include?('changes')
|
30
|
+
yield :rename_changes_to_audited_changes
|
31
|
+
end
|
32
|
+
|
33
|
+
unless columns.include?('remote_address')
|
34
|
+
yield :add_remote_address_to_audits
|
35
|
+
end
|
36
|
+
|
37
|
+
unless columns.include?('request_uuid')
|
38
|
+
yield :add_request_uuid_to_audits
|
39
|
+
end
|
40
|
+
|
41
|
+
unless columns.include?('association_id')
|
42
|
+
if columns.include?('auditable_parent_id')
|
43
|
+
yield :rename_parent_to_association
|
44
|
+
else
|
45
|
+
unless columns.include?('associated_id')
|
46
|
+
yield :add_association_to_audits
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
if columns.include?('association_id')
|
52
|
+
yield :rename_association_to_associated
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Audited::Audit do
|
4
|
+
let(:user) { Models::ActiveRecord::User.new name: "Testing" }
|
5
|
+
|
6
|
+
describe "user=" do
|
7
|
+
|
8
|
+
it "should be able to set the user to a model object" do
|
9
|
+
subject.user = user
|
10
|
+
expect(subject.user).to eq(user)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should be able to set the user to nil" do
|
14
|
+
subject.user_id = 1
|
15
|
+
subject.user_type = 'Models::ActiveRecord::User'
|
16
|
+
subject.username = 'joe'
|
17
|
+
|
18
|
+
subject.user = nil
|
19
|
+
|
20
|
+
expect(subject.user).to be_nil
|
21
|
+
expect(subject.user_id).to be_nil
|
22
|
+
expect(subject.user_type).to be_nil
|
23
|
+
expect(subject.username).to be_nil
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should be able to set the user to a string" do
|
27
|
+
subject.user = 'test'
|
28
|
+
expect(subject.user).to eq('test')
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should clear model when setting to a string" do
|
32
|
+
subject.user = user
|
33
|
+
subject.user = 'testing'
|
34
|
+
expect(subject.user_id).to be_nil
|
35
|
+
expect(subject.user_type).to be_nil
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should clear the username when setting to a model" do
|
39
|
+
subject.username = 'test'
|
40
|
+
subject.user = user
|
41
|
+
expect(subject.username).to be_nil
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "revision" do
|
47
|
+
|
48
|
+
it "should recreate attributes" do
|
49
|
+
user = Models::ActiveRecord::User.create name: "1"
|
50
|
+
5.times {|i| user.update_attribute :name, (i + 2).to_s }
|
51
|
+
|
52
|
+
user.audits.each do |audit|
|
53
|
+
expect(audit.revision.name).to eq(audit.version.to_s)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should set protected attributes" do
|
58
|
+
u = Models::ActiveRecord::User.create(name: "Brandon")
|
59
|
+
u.update_attribute :logins, 1
|
60
|
+
u.update_attribute :logins, 2
|
61
|
+
|
62
|
+
expect(u.audits[2].revision.logins).to eq(2)
|
63
|
+
expect(u.audits[1].revision.logins).to eq(1)
|
64
|
+
expect(u.audits[0].revision.logins).to eq(0)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should bypass attribute assignment wrappers" do
|
68
|
+
u = Models::ActiveRecord::User.create(name: "<Joe>")
|
69
|
+
expect(u.audits.first.revision.name).to eq("<Joe>")
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should work for deleted records" do
|
73
|
+
user = Models::ActiveRecord::User.create name: "1"
|
74
|
+
user.destroy
|
75
|
+
revision = user.audits.last.revision
|
76
|
+
expect(revision.name).to eq(user.name)
|
77
|
+
expect(revision).to be_a_new_record
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should set the version number on create" do
|
82
|
+
user = Models::ActiveRecord::User.create! name: "Set Version Number"
|
83
|
+
expect(user.audits.first.version).to eq(1)
|
84
|
+
user.update_attribute :name, "Set to 2"
|
85
|
+
expect(user.audits.reload.first.version).to eq(1)
|
86
|
+
expect(user.audits.reload.last.version).to eq(2)
|
87
|
+
user.destroy
|
88
|
+
expect(Audited::Audit.where(auditable_type: "Models::ActiveRecord::User", auditable_id: user.id).last.version).to eq(3)
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should set the request uuid on create" do
|
92
|
+
user = Models::ActiveRecord::User.create! name: "Set Request UUID"
|
93
|
+
expect(user.audits.reload.first.request_uuid).not_to be_blank
|
94
|
+
end
|
95
|
+
|
96
|
+
describe "reconstruct_attributes" do
|
97
|
+
it "should work with the old way of storing just the new value" do
|
98
|
+
audits = Audited::Audit.reconstruct_attributes([Audited::Audit.new(audited_changes: {"attribute" => "value"})])
|
99
|
+
expect(audits["attribute"]).to eq("value")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "audited_classes" do
|
104
|
+
class Models::ActiveRecord::CustomUser < ::ActiveRecord::Base
|
105
|
+
end
|
106
|
+
class Models::ActiveRecord::CustomUserSubclass < Models::ActiveRecord::CustomUser
|
107
|
+
audited
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should include audited classes" do
|
111
|
+
expect(Audited::Audit.audited_classes).to include(Models::ActiveRecord::User)
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should include subclasses" do
|
115
|
+
expect(Audited::Audit.audited_classes).to include(Models::ActiveRecord::CustomUserSubclass)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe "new_attributes" do
|
120
|
+
it "should return a hash of the new values" do
|
121
|
+
new_attributes = Audited::Audit.new(audited_changes: {a: [1, 2], b: [3, 4]}).new_attributes
|
122
|
+
expect(new_attributes).to eq({"a" => 2, "b" => 4})
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe "old_attributes" do
|
127
|
+
it "should return a hash of the old values" do
|
128
|
+
old_attributes = Audited::Audit.new(audited_changes: {a: [1, 2], b: [3, 4]}).old_attributes
|
129
|
+
expect(old_attributes).to eq({"a" => 1, "b" => 3})
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
describe "as_user" do
|
134
|
+
it "should record user objects" do
|
135
|
+
Audited::Audit.as_user(user) do
|
136
|
+
company = Models::ActiveRecord::Company.create name: "The auditors"
|
137
|
+
company.name = "The Auditors, Inc"
|
138
|
+
company.save
|
139
|
+
|
140
|
+
company.audits.each do |audit|
|
141
|
+
expect(audit.user).to eq(user)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
it "should record usernames" do
|
147
|
+
Audited::Audit.as_user(user.name) do
|
148
|
+
company = Models::ActiveRecord::Company.create name: "The auditors"
|
149
|
+
company.name = "The Auditors, Inc"
|
150
|
+
company.save
|
151
|
+
|
152
|
+
company.audits.each do |audit|
|
153
|
+
expect(audit.username).to eq(user.name)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
it "should be thread safe" do
|
159
|
+
begin
|
160
|
+
expect(user.save).to eq(true)
|
161
|
+
|
162
|
+
t1 = Thread.new do
|
163
|
+
Audited::Audit.as_user(user) do
|
164
|
+
sleep 1
|
165
|
+
expect(Models::ActiveRecord::Company.create(name: "The Auditors, Inc").audits.first.user).to eq(user)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
t2 = Thread.new do
|
170
|
+
Audited::Audit.as_user(user.name) do
|
171
|
+
expect(Models::ActiveRecord::Company.create(name: "The Competing Auditors, LLC").audits.first.username).to eq(user.name)
|
172
|
+
sleep 0.5
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
t1.join
|
177
|
+
t2.join
|
178
|
+
end
|
179
|
+
end if ActiveRecord::Base.connection.adapter_name != 'SQLite'
|
180
|
+
|
181
|
+
it "should return the value from the yield block" do
|
182
|
+
result = Audited::Audit.as_user('foo') do
|
183
|
+
42
|
184
|
+
end
|
185
|
+
expect(result).to eq(42)
|
186
|
+
end
|
187
|
+
|
188
|
+
it "should reset audited_user when the yield block raises an exception" do
|
189
|
+
expect {
|
190
|
+
Audited::Audit.as_user('foo') do
|
191
|
+
raise StandardError.new('expected')
|
192
|
+
end
|
193
|
+
}.to raise_exception('expected')
|
194
|
+
expect(Thread.current[:audited_user]).to be_nil
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|
@@ -0,0 +1,607 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Audited::Auditor do
|
4
|
+
|
5
|
+
describe "configuration" do
|
6
|
+
it "should include instance methods" do
|
7
|
+
expect(Models::ActiveRecord::User.new).to be_a_kind_of( Audited::Auditor::AuditedInstanceMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should include class methods" do
|
11
|
+
expect(Models::ActiveRecord::User).to be_a_kind_of( Audited::Auditor::AuditedClassMethods )
|
12
|
+
end
|
13
|
+
|
14
|
+
['created_at', 'updated_at', 'created_on', 'updated_on', 'lock_version', 'id', 'password'].each do |column|
|
15
|
+
it "should not audit #{column}" do
|
16
|
+
expect(Models::ActiveRecord::User.non_audited_columns).to include(column)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should be configurable which attributes are not audited" do
|
21
|
+
Audited.ignored_attributes = ['delta', 'top_secret', 'created_at']
|
22
|
+
class Secret < ::ActiveRecord::Base
|
23
|
+
audited
|
24
|
+
end
|
25
|
+
|
26
|
+
expect(Secret.non_audited_columns).to include('delta', 'top_secret', 'created_at')
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should not save non-audited columns" do
|
30
|
+
expect(create_user.audits.first.audited_changes.keys.any? { |col| ['created_at', 'updated_at', 'password'].include?( col ) }).to eq(false)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should not save other columns than specified in 'only' option" do
|
34
|
+
user = Models::ActiveRecord::UserOnlyPassword.create
|
35
|
+
user.instance_eval do
|
36
|
+
def non_column_attr
|
37
|
+
@non_column_attr
|
38
|
+
end
|
39
|
+
|
40
|
+
def non_column_attr=(val)
|
41
|
+
attribute_will_change!("non_column_attr")
|
42
|
+
@non_column_attr = val
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
user.password = "password"
|
47
|
+
user.non_column_attr = "some value"
|
48
|
+
user.save!
|
49
|
+
expect(user.audits.last.audited_changes.keys).to eq(%w{password})
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe :new do
|
54
|
+
it "should allow mass assignment of all unprotected attributes" do
|
55
|
+
yesterday = 1.day.ago
|
56
|
+
|
57
|
+
u = Models::ActiveRecord::NoAttributeProtectionUser.new(name: 'name',
|
58
|
+
username: 'username',
|
59
|
+
password: 'password',
|
60
|
+
activated: true,
|
61
|
+
suspended_at: yesterday,
|
62
|
+
logins: 2)
|
63
|
+
|
64
|
+
expect(u.name).to eq('name')
|
65
|
+
expect(u.username).to eq('username')
|
66
|
+
expect(u.password).to eq('password')
|
67
|
+
expect(u.activated).to eq(true)
|
68
|
+
expect(u.suspended_at.to_i).to eq(yesterday.to_i)
|
69
|
+
expect(u.logins).to eq(2)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "on create" do
|
74
|
+
let( :user ) { create_user audit_comment: "Create" }
|
75
|
+
|
76
|
+
it "should change the audit count" do
|
77
|
+
expect {
|
78
|
+
user
|
79
|
+
}.to change( Audited::Audit, :count ).by(1)
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should create associated audit" do
|
83
|
+
expect(user.audits.count).to eq(1)
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should set the action to create" do
|
87
|
+
expect(user.audits.first.action).to eq('create')
|
88
|
+
expect(Audited::Audit.creates.order(:id).last).to eq(user.audits.first)
|
89
|
+
expect(user.audits.creates.count).to eq(1)
|
90
|
+
expect(user.audits.updates.count).to eq(0)
|
91
|
+
expect(user.audits.destroys.count).to eq(0)
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should store all the audited attributes" do
|
95
|
+
expect(user.audits.first.audited_changes).to eq(user.audited_attributes)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should store comment" do
|
99
|
+
expect(user.audits.first.comment).to eq('Create')
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should not audit an attribute which is excepted if specified on create or destroy" do
|
103
|
+
on_create_destroy_except_name = Models::ActiveRecord::OnCreateDestroyExceptName.create(name: 'Bart')
|
104
|
+
expect(on_create_destroy_except_name.audits.first.audited_changes.keys.any?{|col| ['name'].include? col}).to eq(false)
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should not save an audit if only specified on update/destroy" do
|
108
|
+
expect {
|
109
|
+
Models::ActiveRecord::OnUpdateDestroy.create!( name: 'Bart' )
|
110
|
+
}.to_not change( Audited::Audit, :count )
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe "on update" do
|
115
|
+
before do
|
116
|
+
@user = create_user( name: 'Brandon', audit_comment: 'Update' )
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should save an audit" do
|
120
|
+
expect {
|
121
|
+
@user.update_attribute(:name, "Someone")
|
122
|
+
}.to change( Audited::Audit, :count ).by(1)
|
123
|
+
expect {
|
124
|
+
@user.update_attribute(:name, "Someone else")
|
125
|
+
}.to change( Audited::Audit, :count ).by(1)
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should set the action to 'update'" do
|
129
|
+
@user.update_attributes name: 'Changed'
|
130
|
+
expect(@user.audits.last.action).to eq('update')
|
131
|
+
expect(Audited::Audit.updates.order(:id).last).to eq(@user.audits.last)
|
132
|
+
expect(@user.audits.updates.last).to eq(@user.audits.last)
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should store the changed attributes" do
|
136
|
+
@user.update_attributes name: 'Changed'
|
137
|
+
expect(@user.audits.last.audited_changes).to eq({ 'name' => ['Brandon', 'Changed'] })
|
138
|
+
end
|
139
|
+
|
140
|
+
it "should store audit comment" do
|
141
|
+
expect(@user.audits.last.comment).to eq('Update')
|
142
|
+
end
|
143
|
+
|
144
|
+
it "should not save an audit if only specified on create/destroy" do
|
145
|
+
on_create_destroy = Models::ActiveRecord::OnCreateDestroy.create( name: 'Bart' )
|
146
|
+
expect {
|
147
|
+
on_create_destroy.update_attributes name: 'Changed'
|
148
|
+
}.to_not change( Audited::Audit, :count )
|
149
|
+
end
|
150
|
+
|
151
|
+
it "should not save an audit if the value doesn't change after type casting" do
|
152
|
+
@user.update_attributes! logins: 0, activated: true
|
153
|
+
expect { @user.update_attribute :logins, '0' }.to_not change( Audited::Audit, :count )
|
154
|
+
expect { @user.update_attribute :activated, 1 }.to_not change( Audited::Audit, :count )
|
155
|
+
expect { @user.update_attribute :activated, '1' }.to_not change( Audited::Audit, :count )
|
156
|
+
end
|
157
|
+
|
158
|
+
describe "with no dirty changes" do
|
159
|
+
it "does not create an audit if the record is not changed" do
|
160
|
+
expect {
|
161
|
+
@user.save!
|
162
|
+
}.to_not change( Audited::Audit, :count )
|
163
|
+
end
|
164
|
+
|
165
|
+
it "creates an audit when an audit comment is present" do
|
166
|
+
expect {
|
167
|
+
@user.audit_comment = "Comment"
|
168
|
+
@user.save!
|
169
|
+
}.to change( Audited::Audit, :count )
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
describe "on destroy" do
|
175
|
+
before do
|
176
|
+
@user = create_user
|
177
|
+
end
|
178
|
+
|
179
|
+
it "should save an audit" do
|
180
|
+
expect {
|
181
|
+
@user.destroy
|
182
|
+
}.to change( Audited::Audit, :count )
|
183
|
+
|
184
|
+
expect(@user.audits.size).to eq(2)
|
185
|
+
end
|
186
|
+
|
187
|
+
it "should set the action to 'destroy'" do
|
188
|
+
@user.destroy
|
189
|
+
|
190
|
+
expect(@user.audits.last.action).to eq('destroy')
|
191
|
+
expect(Audited::Audit.destroys.order(:id).last).to eq(@user.audits.last)
|
192
|
+
expect(@user.audits.destroys.last).to eq(@user.audits.last)
|
193
|
+
end
|
194
|
+
|
195
|
+
it "should store all of the audited attributes" do
|
196
|
+
@user.destroy
|
197
|
+
|
198
|
+
expect(@user.audits.last.audited_changes).to eq(@user.audited_attributes)
|
199
|
+
end
|
200
|
+
|
201
|
+
it "should be able to reconstruct a destroyed record without history" do
|
202
|
+
@user.audits.delete_all
|
203
|
+
@user.destroy
|
204
|
+
|
205
|
+
revision = @user.audits.first.revision
|
206
|
+
expect(revision.name).to eq(@user.name)
|
207
|
+
end
|
208
|
+
|
209
|
+
it "should not save an audit if only specified on create/update" do
|
210
|
+
on_create_update = Models::ActiveRecord::OnCreateUpdate.create!( name: 'Bart' )
|
211
|
+
|
212
|
+
expect {
|
213
|
+
on_create_update.destroy
|
214
|
+
}.to_not change( Audited::Audit, :count )
|
215
|
+
end
|
216
|
+
|
217
|
+
it "should audit dependent destructions" do
|
218
|
+
owner = Models::ActiveRecord::Owner.create!
|
219
|
+
company = owner.companies.create!
|
220
|
+
|
221
|
+
expect {
|
222
|
+
owner.destroy
|
223
|
+
}.to change( Audited::Audit, :count )
|
224
|
+
|
225
|
+
expect(company.audits.map { |a| a.action }).to eq(['create', 'destroy'])
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
describe "on destroy with unsaved object" do
|
230
|
+
let(:user) { Models::ActiveRecord::User.new }
|
231
|
+
|
232
|
+
it "should not audit on 'destroy'" do
|
233
|
+
expect {
|
234
|
+
user.destroy
|
235
|
+
}.to_not raise_error
|
236
|
+
|
237
|
+
expect( user.audits ).to be_empty
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
describe "associated with" do
|
242
|
+
let(:owner) { Models::ActiveRecord::Owner.create(name: 'Models::ActiveRecord::Owner') }
|
243
|
+
let(:owned_company) { Models::ActiveRecord::OwnedCompany.create!(name: 'The auditors', owner: owner) }
|
244
|
+
|
245
|
+
it "should record the associated object on create" do
|
246
|
+
expect(owned_company.audits.first.associated).to eq(owner)
|
247
|
+
end
|
248
|
+
|
249
|
+
it "should store the associated object on update" do
|
250
|
+
owned_company.update_attribute(:name, 'The Auditors')
|
251
|
+
expect(owned_company.audits.last.associated).to eq(owner)
|
252
|
+
end
|
253
|
+
|
254
|
+
it "should store the associated object on destroy" do
|
255
|
+
owned_company.destroy
|
256
|
+
expect(owned_company.audits.last.associated).to eq(owner)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
describe "has associated audits" do
|
261
|
+
let!(:owner) { Models::ActiveRecord::Owner.create!(name: 'Models::ActiveRecord::Owner') }
|
262
|
+
let!(:owned_company) { Models::ActiveRecord::OwnedCompany.create!(name: 'The auditors', owner: owner) }
|
263
|
+
|
264
|
+
it "should list the associated audits" do
|
265
|
+
expect(owner.associated_audits.length).to eq(1)
|
266
|
+
expect(owner.associated_audits.first.auditable).to eq(owned_company)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
describe "revisions" do
|
271
|
+
let( :user ) { create_versions }
|
272
|
+
|
273
|
+
it "should return an Array of Users" do
|
274
|
+
expect(user.revisions).to be_a_kind_of( Array )
|
275
|
+
user.revisions.each { |version| expect(version).to be_a_kind_of Models::ActiveRecord::User }
|
276
|
+
end
|
277
|
+
|
278
|
+
it "should have one revision for a new record" do
|
279
|
+
expect(create_user.revisions.size).to eq(1)
|
280
|
+
end
|
281
|
+
|
282
|
+
it "should have one revision for each audit" do
|
283
|
+
expect(user.audits.size).to eql( user.revisions.size )
|
284
|
+
end
|
285
|
+
|
286
|
+
it "should set the attributes for each revision" do
|
287
|
+
u = Models::ActiveRecord::User.create(name: 'Brandon', username: 'brandon')
|
288
|
+
u.update_attributes name: 'Foobar'
|
289
|
+
u.update_attributes name: 'Awesome', username: 'keepers'
|
290
|
+
|
291
|
+
expect(u.revisions.size).to eql(3)
|
292
|
+
|
293
|
+
expect(u.revisions[0].name).to eql('Brandon')
|
294
|
+
expect(u.revisions[0].username).to eql('brandon')
|
295
|
+
|
296
|
+
expect(u.revisions[1].name).to eql('Foobar')
|
297
|
+
expect(u.revisions[1].username).to eql('brandon')
|
298
|
+
|
299
|
+
expect(u.revisions[2].name).to eql('Awesome')
|
300
|
+
expect(u.revisions[2].username).to eql('keepers')
|
301
|
+
end
|
302
|
+
|
303
|
+
it "access to only recent revisions" do
|
304
|
+
u = Models::ActiveRecord::User.create(name: 'Brandon', username: 'brandon')
|
305
|
+
u.update_attributes name: 'Foobar'
|
306
|
+
u.update_attributes name: 'Awesome', username: 'keepers'
|
307
|
+
|
308
|
+
expect(u.revisions(2).size).to eq(2)
|
309
|
+
|
310
|
+
expect(u.revisions(2)[0].name).to eq('Foobar')
|
311
|
+
expect(u.revisions(2)[0].username).to eq('brandon')
|
312
|
+
|
313
|
+
expect(u.revisions(2)[1].name).to eq('Awesome')
|
314
|
+
expect(u.revisions(2)[1].username).to eq('keepers')
|
315
|
+
end
|
316
|
+
|
317
|
+
it "should be empty if no audits exist" do
|
318
|
+
user.audits.delete_all
|
319
|
+
expect(user.revisions).to be_empty
|
320
|
+
end
|
321
|
+
|
322
|
+
it "should ignore attributes that have been deleted" do
|
323
|
+
user.audits.last.update_attributes audited_changes: {old_attribute: 'old value'}
|
324
|
+
expect { user.revisions }.to_not raise_error
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
describe "revisions" do
|
329
|
+
let( :user ) { create_versions(5) }
|
330
|
+
|
331
|
+
it "should maintain identity" do
|
332
|
+
expect(user.revision(1)).to eq(user)
|
333
|
+
end
|
334
|
+
|
335
|
+
it "should find the given revision" do
|
336
|
+
revision = user.revision(3)
|
337
|
+
expect(revision).to be_a_kind_of( Models::ActiveRecord::User )
|
338
|
+
expect(revision.version).to eq(3)
|
339
|
+
expect(revision.name).to eq('Foobar 3')
|
340
|
+
end
|
341
|
+
|
342
|
+
it "should find the previous revision with :previous" do
|
343
|
+
revision = user.revision(:previous)
|
344
|
+
expect(revision.version).to eq(4)
|
345
|
+
#expect(revision).to eq(user.revision(4))
|
346
|
+
expect(revision.attributes).to eq(user.revision(4).attributes)
|
347
|
+
end
|
348
|
+
|
349
|
+
it "should be able to get the previous revision repeatedly" do
|
350
|
+
previous = user.revision(:previous)
|
351
|
+
expect(previous.version).to eq(4)
|
352
|
+
expect(previous.revision(:previous).version).to eq(3)
|
353
|
+
end
|
354
|
+
|
355
|
+
it "should be able to set protected attributes" do
|
356
|
+
u = Models::ActiveRecord::User.create(name: 'Brandon')
|
357
|
+
u.update_attribute :logins, 1
|
358
|
+
u.update_attribute :logins, 2
|
359
|
+
|
360
|
+
expect(u.revision(3).logins).to eq(2)
|
361
|
+
expect(u.revision(2).logins).to eq(1)
|
362
|
+
expect(u.revision(1).logins).to eq(0)
|
363
|
+
end
|
364
|
+
|
365
|
+
it "should set attributes directly" do
|
366
|
+
u = Models::ActiveRecord::User.create(name: '<Joe>')
|
367
|
+
expect(u.revision(1).name).to eq('<Joe>')
|
368
|
+
end
|
369
|
+
|
370
|
+
it "should set the attributes for each revision" do
|
371
|
+
u = Models::ActiveRecord::User.create(name: 'Brandon', username: 'brandon')
|
372
|
+
u.update_attributes name: 'Foobar'
|
373
|
+
u.update_attributes name: 'Awesome', username: 'keepers'
|
374
|
+
|
375
|
+
expect(u.revision(3).name).to eq('Awesome')
|
376
|
+
expect(u.revision(3).username).to eq('keepers')
|
377
|
+
|
378
|
+
expect(u.revision(2).name).to eq('Foobar')
|
379
|
+
expect(u.revision(2).username).to eq('brandon')
|
380
|
+
|
381
|
+
expect(u.revision(1).name).to eq('Brandon')
|
382
|
+
expect(u.revision(1).username).to eq('brandon')
|
383
|
+
end
|
384
|
+
|
385
|
+
it "should be able to get time for first revision" do
|
386
|
+
suspended_at = Time.zone.now
|
387
|
+
u = Models::ActiveRecord::User.create(suspended_at: suspended_at)
|
388
|
+
expect(u.revision(1).suspended_at.to_s).to eq(suspended_at.to_s)
|
389
|
+
end
|
390
|
+
|
391
|
+
it "should not raise an error when no previous audits exist" do
|
392
|
+
user.audits.destroy_all
|
393
|
+
expect { user.revision(:previous) }.to_not raise_error
|
394
|
+
end
|
395
|
+
|
396
|
+
it "should mark revision's attributes as changed" do
|
397
|
+
expect(user.revision(1).name_changed?).to eq(true)
|
398
|
+
end
|
399
|
+
|
400
|
+
it "should record new audit when saving revision" do
|
401
|
+
expect {
|
402
|
+
user.revision(1).save!
|
403
|
+
}.to change( user.audits, :count ).by(1)
|
404
|
+
end
|
405
|
+
|
406
|
+
it "should re-insert destroyed records" do
|
407
|
+
user.destroy
|
408
|
+
expect {
|
409
|
+
user.revision(1).save!
|
410
|
+
}.to change( Models::ActiveRecord::User, :count ).by(1)
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
describe "revision_at" do
|
415
|
+
let( :user ) { create_user }
|
416
|
+
|
417
|
+
it "should find the latest revision before the given time" do
|
418
|
+
audit = user.audits.first
|
419
|
+
audit.created_at = 1.hour.ago
|
420
|
+
audit.save!
|
421
|
+
user.update_attributes name: 'updated'
|
422
|
+
expect(user.revision_at( 2.minutes.ago ).version).to eq(1)
|
423
|
+
end
|
424
|
+
|
425
|
+
it "should be nil if given a time before audits" do
|
426
|
+
expect(user.revision_at( 1.week.ago )).to be_nil
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
describe "without auditing" do
|
431
|
+
it "should not save an audit when calling #save_without_auditing" do
|
432
|
+
expect {
|
433
|
+
u = Models::ActiveRecord::User.new(name: 'Brandon')
|
434
|
+
expect(u.save_without_auditing).to eq(true)
|
435
|
+
}.to_not change( Audited::Audit, :count )
|
436
|
+
end
|
437
|
+
|
438
|
+
it "should not save an audit inside of the #without_auditing block" do
|
439
|
+
expect {
|
440
|
+
Models::ActiveRecord::User.without_auditing { Models::ActiveRecord::User.create!( name: 'Brandon' ) }
|
441
|
+
}.to_not change( Audited::Audit, :count )
|
442
|
+
end
|
443
|
+
|
444
|
+
it "should reset auditing status even it raises an exception" do
|
445
|
+
Models::ActiveRecord::User.without_auditing { raise } rescue nil
|
446
|
+
expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
|
447
|
+
end
|
448
|
+
|
449
|
+
it "should be thread safe using a #without_auditing block" do
|
450
|
+
begin
|
451
|
+
t1 = Thread.new do
|
452
|
+
expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
|
453
|
+
Models::ActiveRecord::User.without_auditing do
|
454
|
+
expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
|
455
|
+
Models::ActiveRecord::User.create!( name: 'Bart' )
|
456
|
+
sleep 1
|
457
|
+
expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
|
458
|
+
end
|
459
|
+
expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
|
460
|
+
end
|
461
|
+
|
462
|
+
t2 = Thread.new do
|
463
|
+
sleep 0.5
|
464
|
+
expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
|
465
|
+
Models::ActiveRecord::User.create!( name: 'Lisa' )
|
466
|
+
end
|
467
|
+
t1.join
|
468
|
+
t2.join
|
469
|
+
|
470
|
+
expect(Models::ActiveRecord::User.find_by_name('Bart').audits.count).to eq(0)
|
471
|
+
expect(Models::ActiveRecord::User.find_by_name('Lisa').audits.count).to eq(1)
|
472
|
+
rescue ActiveRecord::StatementInvalid
|
473
|
+
STDERR.puts "Thread safety tests cannot be run with SQLite"
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
describe "comment required" do
|
479
|
+
|
480
|
+
describe "on create" do
|
481
|
+
it "should not validate when audit_comment is not supplied" do
|
482
|
+
expect(Models::ActiveRecord::CommentRequiredUser.new).not_to be_valid
|
483
|
+
end
|
484
|
+
|
485
|
+
it "should validate when audit_comment is supplied" do
|
486
|
+
expect(Models::ActiveRecord::CommentRequiredUser.new( audit_comment: 'Create')).to be_valid
|
487
|
+
end
|
488
|
+
|
489
|
+
it "should validate when audit_comment is not supplied, and auditing is disabled" do
|
490
|
+
Models::ActiveRecord::CommentRequiredUser.disable_auditing
|
491
|
+
expect(Models::ActiveRecord::CommentRequiredUser.new).to be_valid
|
492
|
+
Models::ActiveRecord::CommentRequiredUser.enable_auditing
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
describe "on update" do
|
497
|
+
let( :user ) { Models::ActiveRecord::CommentRequiredUser.create!( audit_comment: 'Create' ) }
|
498
|
+
|
499
|
+
it "should not validate when audit_comment is not supplied" do
|
500
|
+
expect(user.update_attributes(name: 'Test')).to eq(false)
|
501
|
+
end
|
502
|
+
|
503
|
+
it "should validate when audit_comment is supplied" do
|
504
|
+
expect(user.update_attributes(name: 'Test', audit_comment: 'Update')).to eq(true)
|
505
|
+
end
|
506
|
+
|
507
|
+
it "should validate when audit_comment is not supplied, and auditing is disabled" do
|
508
|
+
Models::ActiveRecord::CommentRequiredUser.disable_auditing
|
509
|
+
expect(user.update_attributes(name: 'Test')).to eq(true)
|
510
|
+
Models::ActiveRecord::CommentRequiredUser.enable_auditing
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
describe "on destroy" do
|
515
|
+
let( :user ) { Models::ActiveRecord::CommentRequiredUser.create!( audit_comment: 'Create' )}
|
516
|
+
|
517
|
+
it "should not validate when audit_comment is not supplied" do
|
518
|
+
expect(user.destroy).to eq(false)
|
519
|
+
end
|
520
|
+
|
521
|
+
it "should validate when audit_comment is supplied" do
|
522
|
+
user.audit_comment = "Destroy"
|
523
|
+
expect(user.destroy).to eq(user)
|
524
|
+
end
|
525
|
+
|
526
|
+
it "should validate when audit_comment is not supplied, and auditing is disabled" do
|
527
|
+
Models::ActiveRecord::CommentRequiredUser.disable_auditing
|
528
|
+
expect(user.destroy).to eq(user)
|
529
|
+
Models::ActiveRecord::CommentRequiredUser.enable_auditing
|
530
|
+
end
|
531
|
+
end
|
532
|
+
|
533
|
+
end
|
534
|
+
|
535
|
+
describe "attr_protected and attr_accessible" do
|
536
|
+
|
537
|
+
it "should not raise error when attr_accessible is set and protected is false" do
|
538
|
+
expect {
|
539
|
+
Models::ActiveRecord::AccessibleAfterDeclarationUser.new(name: 'No fail!')
|
540
|
+
}.to_not raise_error
|
541
|
+
end
|
542
|
+
|
543
|
+
it "should not rause an error when attr_accessible is declared before audited" do
|
544
|
+
expect {
|
545
|
+
Models::ActiveRecord::AccessibleAfterDeclarationUser.new(name: 'No fail!')
|
546
|
+
}.to_not raise_error
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
describe "audit_as" do
|
551
|
+
let( :user ) { Models::ActiveRecord::User.create name: 'Testing' }
|
552
|
+
|
553
|
+
it "should record user objects" do
|
554
|
+
Models::ActiveRecord::Company.audit_as( user ) do
|
555
|
+
company = Models::ActiveRecord::Company.create name: 'The auditors'
|
556
|
+
company.update_attributes name: 'The Auditors'
|
557
|
+
|
558
|
+
company.audits.each do |audit|
|
559
|
+
expect(audit.user).to eq(user)
|
560
|
+
end
|
561
|
+
end
|
562
|
+
end
|
563
|
+
|
564
|
+
it "should record usernames" do
|
565
|
+
Models::ActiveRecord::Company.audit_as( user.name ) do
|
566
|
+
company = Models::ActiveRecord::Company.create name: 'The auditors'
|
567
|
+
company.update_attributes name: 'The Auditors'
|
568
|
+
|
569
|
+
company.audits.each do |audit|
|
570
|
+
expect(audit.user).to eq(user.name)
|
571
|
+
end
|
572
|
+
end
|
573
|
+
end
|
574
|
+
end
|
575
|
+
|
576
|
+
describe "after_audit" do
|
577
|
+
let( :user ) { user = Models::ActiveRecord::UserWithAfterAudit.new }
|
578
|
+
|
579
|
+
it "should invoke after_audit callback on create" do
|
580
|
+
expect(user.bogus_attr).to be_nil
|
581
|
+
expect(user.save).to eq(true)
|
582
|
+
expect(user.bogus_attr).to eq("do something")
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
describe "around_audit" do
|
587
|
+
let( :user ) { user = Models::ActiveRecord::UserWithAfterAudit.new }
|
588
|
+
|
589
|
+
it "should invoke around_audit callback on create" do
|
590
|
+
expect(user.around_attr).to be_nil
|
591
|
+
expect(user.save).to eq(true)
|
592
|
+
expect(user.around_attr).to eq(user.audits.last)
|
593
|
+
end
|
594
|
+
end
|
595
|
+
|
596
|
+
describe "STI auditing" do
|
597
|
+
it "should correctly disable auditing when using STI" do
|
598
|
+
company = Models::ActiveRecord::Company::STICompany.create name: 'The auditors'
|
599
|
+
expect(company.type).to eq("Models::ActiveRecord::Company::STICompany")
|
600
|
+
expect {
|
601
|
+
Models::ActiveRecord::Company.auditing_enabled = false
|
602
|
+
company.update_attributes name: 'STI auditors'
|
603
|
+
Models::ActiveRecord::Company.auditing_enabled = true
|
604
|
+
}.to_not change( Audited.audit_class, :count )
|
605
|
+
end
|
606
|
+
end
|
607
|
+
end
|