├── spec
├── rails_app
│ ├── app
│ │ ├── assets
│ │ │ └── config
│ │ │ │ └── manifest.js
│ │ └── controllers
│ │ │ └── application_controller.rb
│ └── config
│ │ ├── routes.rb
│ │ ├── initializers
│ │ ├── inflections.rb
│ │ ├── secret_token.rb
│ │ └── backtrace_silencers.rb
│ │ ├── environment.rb
│ │ ├── application.rb
│ │ ├── database.yml
│ │ └── environments
│ │ ├── development.rb
│ │ ├── production.rb
│ │ └── test.rb
├── support
│ └── active_record
│ │ ├── postgres
│ │ ├── 1_change_audited_changes_type_to_json.rb
│ │ └── 2_change_audited_changes_type_to_jsonb.rb
│ │ ├── schema.rb
│ │ └── models.rb
├── spec_helper.rb
├── audited_spec_helpers.rb
└── audited
│ ├── rspec_matchers_spec.rb
│ ├── sweeper_spec.rb
│ ├── audit_spec.rb
│ └── auditor_spec.rb
├── lib
├── audited
│ ├── version.rb
│ ├── sweeper.rb
│ ├── audit.rb
│ ├── rspec_matchers.rb
│ └── auditor.rb
├── audited-rspec.rb
├── generators
│ └── audited
│ │ ├── templates
│ │ ├── add_comment_to_audits.rb
│ │ ├── add_remote_address_to_audits.rb
│ │ ├── rename_changes_to_audited_changes.rb
│ │ ├── add_request_uuid_to_audits.rb
│ │ ├── add_association_to_audits.rb
│ │ ├── rename_parent_to_association.rb
│ │ ├── add_version_to_auditable_index.rb
│ │ ├── revert_polymorphic_indexes_order.rb
│ │ ├── rename_association_to_associated.rb
│ │ └── install.rb
│ │ ├── migration_helper.rb
│ │ ├── migration.rb
│ │ ├── install_generator.rb
│ │ └── upgrade_generator.rb
└── audited.rb
├── Gemfile
├── .yardopts
├── .gitignore
├── gemfiles
├── rails61.gemfile
├── rails50.gemfile
├── rails51.gemfile
├── rails60.gemfile
├── rails52.gemfile
└── rails42.gemfile
├── Rakefile
├── test
├── test_helper.rb
├── db
│ ├── version_5.rb
│ ├── version_1.rb
│ ├── version_2.rb
│ ├── version_3.rb
│ ├── version_6.rb
│ └── version_4.rb
├── install_generator_test.rb
└── upgrade_generator_test.rb
├── .rubocop.yml
├── LICENSE
├── Appraisals
├── audited.gemspec
├── .travis.yml
├── CHANGELOG.md
└── README.md
/spec/rails_app/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/lib/audited/version.rb:
--------------------------------------------------------------------------------
1 | module Audited
2 | VERSION = "4.9.0"
3 | end
4 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec name: "audited"
4 |
--------------------------------------------------------------------------------
/.yardopts:
--------------------------------------------------------------------------------
1 | --no-private
2 | --title acts_as_audited
3 | --exclude lib/generators
4 |
--------------------------------------------------------------------------------
/spec/rails_app/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resources :audits
3 | end
4 |
--------------------------------------------------------------------------------
/spec/rails_app/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | ActiveSupport::Inflector.inflections do |inflect|
2 | end
3 |
--------------------------------------------------------------------------------
/spec/rails_app/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | end
3 |
--------------------------------------------------------------------------------
/lib/audited-rspec.rb:
--------------------------------------------------------------------------------
1 | require 'audited/rspec_matchers'
2 | module RSpec::Matchers
3 | include Audited::RspecMatchers
4 | end
5 |
--------------------------------------------------------------------------------
/spec/rails_app/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the rails application
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the rails application
5 | RailsApp::Application.initialize!
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | *.ipr
3 | *.iws
4 | *.log
5 | *.swp
6 | .bundle
7 | .rakeTasks
8 | .ruby-gemset
9 | .ruby-version
10 | .rvmrc
11 | .yardoc
12 | doc/
13 | Gemfile.lock
14 | gemfiles/*.lock
15 | pkg
16 | tmp/*
17 | audited_test.sqlite3.db
18 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/add_comment_to_audits.rb:
--------------------------------------------------------------------------------
1 | class <%= migration_class_name %> < <%= migration_parent %>
2 | def self.up
3 | add_column :audits, :comment, :string
4 | end
5 |
6 | def self.down
7 | remove_column :audits, :comment
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/gemfiles/rails61.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", ">= 6.1.0", "< 6.2"
6 | gem "mysql2", ">= 0.4.4"
7 | gem "pg", ">= 1.1", "< 2.0"
8 | gem "sqlite3", "~> 1.4"
9 |
10 | gemspec name: "audited", path: "../"
11 |
--------------------------------------------------------------------------------
/gemfiles/rails50.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", "~> 5.0.0"
6 | gem "mysql2", ">= 0.3.18", "< 0.6.0"
7 | gem "pg", ">= 0.18", "< 2.0"
8 | gem "sqlite3", "~> 1.3.6"
9 |
10 | gemspec name: "audited", path: "../"
11 |
--------------------------------------------------------------------------------
/gemfiles/rails51.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", "~> 5.1.4"
6 | gem "mysql2", ">= 0.3.18", "< 0.6.0"
7 | gem "pg", ">= 0.18", "< 2.0"
8 | gem "sqlite3", "~> 1.3.6"
9 |
10 | gemspec name: "audited", path: "../"
11 |
--------------------------------------------------------------------------------
/gemfiles/rails60.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", ">= 6.0.0.rc1", "< 6.1"
6 | gem "mysql2", ">= 0.4.4"
7 | gem "pg", ">= 0.18", "< 2.0"
8 | gem "sqlite3", "~> 1.4"
9 |
10 | gemspec name: "audited", path: "../"
11 |
--------------------------------------------------------------------------------
/gemfiles/rails52.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", ">= 5.2.0", "< 5.3"
6 | gem "mysql2", ">= 0.4.4", "< 0.6.0"
7 | gem "pg", ">= 0.18", "< 2.0"
8 | gem "sqlite3", "~> 1.3.6"
9 |
10 | gemspec name: "audited", path: "../"
11 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/add_remote_address_to_audits.rb:
--------------------------------------------------------------------------------
1 | class <%= migration_class_name %> < <%= migration_parent %>
2 | def self.up
3 | add_column :audits, :remote_address, :string
4 | end
5 |
6 | def self.down
7 | remove_column :audits, :remote_address
8 | end
9 | end
10 |
11 |
--------------------------------------------------------------------------------
/gemfiles/rails42.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", "~> 4.2.0"
6 | gem "protected_attributes"
7 | gem "mysql2", ">= 0.3.13", "< 0.6.0"
8 | gem "pg", "~> 0.15"
9 | gem "sqlite3", "~> 1.3.6"
10 |
11 | gemspec name: "audited", path: "../"
12 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/rename_changes_to_audited_changes.rb:
--------------------------------------------------------------------------------
1 | class <%= migration_class_name %> < <%= migration_parent %>
2 | def self.up
3 | rename_column :audits, :changes, :audited_changes
4 | end
5 |
6 | def self.down
7 | rename_column :audits, :audited_changes, :changes
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/generators/audited/migration_helper.rb:
--------------------------------------------------------------------------------
1 | module Audited
2 | module Generators
3 | module MigrationHelper
4 | def migration_parent
5 | Rails::VERSION::MAJOR == 4 ? 'ActiveRecord::Migration' : "ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]"
6 | end
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/add_request_uuid_to_audits.rb:
--------------------------------------------------------------------------------
1 | class <%= migration_class_name %> < <%= migration_parent %>
2 | def self.up
3 | add_column :audits, :request_uuid, :string
4 | add_index :audits, :request_uuid
5 | end
6 |
7 | def self.down
8 | remove_column :audits, :request_uuid
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/rails_app/config/initializers/secret_token.rb:
--------------------------------------------------------------------------------
1 | Rails.application.config.secret_token = 'ea942c41850d502f2c8283e26bdc57829f471bb18224ddff0a192c4f32cdf6cb5aa0d82b3a7a7adbeb640c4b06f3aa1cd5f098162d8240f669b39d6b49680571'
2 | Rails.application.config.session_store :cookie_store, key: "_my_app"
3 | Rails.application.config.secret_key_base = 'secret value'
4 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/add_association_to_audits.rb:
--------------------------------------------------------------------------------
1 | class <%= migration_class_name %> < <%= migration_parent %>
2 | def self.up
3 | add_column :audits, :association_id, :integer
4 | add_column :audits, :association_type, :string
5 | end
6 |
7 | def self.down
8 | remove_column :audits, :association_type
9 | remove_column :audits, :association_id
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 |
3 | require 'bundler/gem_helper'
4 | require 'rspec/core/rake_task'
5 | require 'rake/testtask'
6 | require 'appraisal'
7 |
8 | Bundler::GemHelper.install_tasks(name: 'audited')
9 |
10 | RSpec::Core::RakeTask.new(:spec)
11 |
12 | Rake::TestTask.new do |t|
13 | t.libs << "test"
14 | t.test_files = FileList['test/**/*_test.rb']
15 | t.verbose = true
16 | end
17 |
18 | task default: [:spec, :test]
19 |
--------------------------------------------------------------------------------
/spec/rails_app/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/rename_parent_to_association.rb:
--------------------------------------------------------------------------------
1 | class <%= migration_class_name %> < <%= migration_parent %>
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
12 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | ENV['RAILS_ENV'] = 'test'
2 |
3 | $LOAD_PATH.unshift File.dirname(__FILE__)
4 |
5 | require File.expand_path('../../spec/rails_app/config/environment', __FILE__)
6 | require 'rails/test_help'
7 |
8 | require 'audited'
9 |
10 | class ActiveSupport::TestCase
11 | setup do
12 | ActiveRecord::Migration.verbose = false
13 | end
14 |
15 | def load_schema( version )
16 | load File.dirname(__FILE__) + "/db/version_#{version}.rb"
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | DisplayCopNames: true
3 | TargetRubyVersion: 2.3
4 | Exclude:
5 | - lib/generators/audited/templates/**/*
6 | - vendor/bundle/**/*
7 | - gemfiles/vendor/bundle/**/*
8 |
9 | Bundler/OrderedGems:
10 | Enabled: false
11 |
12 | Gemspec/OrderedDependencies:
13 | Enabled: false
14 |
15 | Layout:
16 | Enabled: false
17 |
18 | Metrics:
19 | Enabled: false
20 |
21 | Naming:
22 | Enabled: false
23 |
24 | Style:
25 | Enabled: false
26 |
--------------------------------------------------------------------------------
/spec/support/active_record/postgres/1_change_audited_changes_type_to_json.rb:
--------------------------------------------------------------------------------
1 | parent = Rails::VERSION::MAJOR == 4 ? ActiveRecord::Migration : ActiveRecord::Migration[4.2]
2 | class ChangeAuditedChangesTypeToJson < parent
3 | def self.up
4 | remove_column :audits, :audited_changes
5 | add_column :audits, :audited_changes, :json
6 | end
7 |
8 | def self.down
9 | remove_column :audits, :audited_changes
10 | add_column :audits, :audited_changes, :text
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/spec/support/active_record/postgres/2_change_audited_changes_type_to_jsonb.rb:
--------------------------------------------------------------------------------
1 | parent = Rails::VERSION::MAJOR == 4 ? ActiveRecord::Migration : ActiveRecord::Migration[4.2]
2 | class ChangeAuditedChangesTypeToJsonb < parent
3 | def self.up
4 | remove_column :audits, :audited_changes
5 | add_column :audits, :audited_changes, :jsonb
6 | end
7 |
8 | def self.down
9 | remove_column :audits, :audited_changes
10 | add_column :audits, :audited_changes, :text
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/spec/rails_app/config/application.rb:
--------------------------------------------------------------------------------
1 | require 'rails/all'
2 |
3 | module RailsApp
4 | class Application < Rails::Application
5 | config.root = File.expand_path('../../', __FILE__)
6 | config.i18n.enforce_available_locales = true
7 | end
8 | end
9 |
10 | require 'active_record/connection_adapters/sqlite3_adapter'
11 | if ActiveRecord::ConnectionAdapters::SQLite3Adapter.respond_to?(:represent_boolean_as_integer)
12 | ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = true
13 | end
14 |
--------------------------------------------------------------------------------
/spec/rails_app/config/database.yml:
--------------------------------------------------------------------------------
1 | sqlite3mem: &SQLITE3MEM
2 | adapter: sqlite3
3 | database: ":memory:"
4 |
5 | sqlite3: &SQLITE
6 | adapter: sqlite3
7 | database: audited_test.sqlite3.db
8 |
9 | postgresql: &POSTGRES
10 | adapter: postgresql
11 | username: postgres
12 | password:
13 | database: audited_test
14 | min_messages: ERROR
15 |
16 | mysql: &MYSQL
17 | adapter: mysql2
18 | host: localhost
19 | username: root
20 | password:
21 | database: audited_test
22 | charset: utf8
23 |
24 | test:
25 | <<: *<%= ENV['DB'] || 'SQLITE3MEM' %>
26 |
--------------------------------------------------------------------------------
/lib/generators/audited/migration.rb:
--------------------------------------------------------------------------------
1 | module Audited
2 | module Generators
3 | module Migration
4 | # Implement the required interface for Rails::Generators::Migration.
5 | def next_migration_number(dirname) #:nodoc:
6 | next_migration_number = current_migration_number(dirname) + 1
7 | if ::ActiveRecord::Base.timestamped_migrations
8 | [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
9 | else
10 | "%.3d" % next_migration_number
11 | end
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/test/db/version_5.rb:
--------------------------------------------------------------------------------
1 | ActiveRecord::Schema.define do
2 | create_table :audits, force: true do |t|
3 | t.column :auditable_id, :integer
4 | t.column :auditable_type, :string
5 | t.column :user_id, :integer
6 | t.column :user_type, :string
7 | t.column :username, :string
8 | t.column :action, :string
9 | t.column :audited_changes, :text
10 | t.column :version, :integer, default: 0
11 | t.column :comment, :string
12 | t.column :created_at, :datetime
13 | t.column :remote_address, :string
14 | t.column :association_id, :integer
15 | t.column :association_type, :string
16 | end
17 | end
18 |
19 |
--------------------------------------------------------------------------------
/test/db/version_1.rb:
--------------------------------------------------------------------------------
1 | ActiveRecord::Schema.define do
2 | create_table :audits, force: true do |t|
3 | t.column :auditable_id, :integer
4 | t.column :auditable_type, :string
5 | t.column :user_id, :integer
6 | t.column :user_type, :string
7 | t.column :username, :string
8 | t.column :action, :string
9 | t.column :changes, :text
10 | t.column :version, :integer, default: 0
11 | t.column :created_at, :datetime
12 | end
13 |
14 | add_index :audits, [:auditable_id, :auditable_type], name: 'auditable_index'
15 | add_index :audits, [:user_id, :user_type], name: 'user_index'
16 | add_index :audits, :created_at
17 | end
18 |
--------------------------------------------------------------------------------
/test/db/version_2.rb:
--------------------------------------------------------------------------------
1 | ActiveRecord::Schema.define do
2 | create_table :audits, force: true do |t|
3 | t.column :auditable_id, :integer
4 | t.column :auditable_type, :string
5 | t.column :user_id, :integer
6 | t.column :user_type, :string
7 | t.column :username, :string
8 | t.column :action, :string
9 | t.column :changes, :text
10 | t.column :version, :integer, default: 0
11 | t.column :comment, :string
12 | t.column :created_at, :datetime
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
18 | end
19 |
--------------------------------------------------------------------------------
/test/db/version_3.rb:
--------------------------------------------------------------------------------
1 | ActiveRecord::Schema.define do
2 | create_table :audits, force: true do |t|
3 | t.column :auditable_id, :integer
4 | t.column :auditable_type, :string
5 | t.column :user_id, :integer
6 | t.column :user_type, :string
7 | t.column :username, :string
8 | t.column :action, :string
9 | t.column :audited_changes, :text
10 | t.column :version, :integer, default: 0
11 | t.column :comment, :string
12 | t.column :created_at, :datetime
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
18 | end
19 |
20 |
--------------------------------------------------------------------------------
/test/db/version_6.rb:
--------------------------------------------------------------------------------
1 | ActiveRecord::Schema.define do
2 | create_table :audits, force: true do |t|
3 | t.column :auditable_id, :integer
4 | t.column :auditable_type, :string
5 | t.column :user_id, :integer
6 | t.column :user_type, :string
7 | t.column :username, :string
8 | t.column :action, :string
9 | t.column :audited_changes, :text
10 | t.column :version, :integer, default: 0
11 | t.column :comment, :string
12 | t.column :created_at, :datetime
13 | t.column :remote_address, :string
14 | t.column :associated_id, :integer
15 | t.column :associated_type, :string
16 | end
17 |
18 | add_index :audits, [:auditable_type, :auditable_id], name: 'auditable_index'
19 | end
20 |
--------------------------------------------------------------------------------
/test/db/version_4.rb:
--------------------------------------------------------------------------------
1 | ActiveRecord::Schema.define do
2 | create_table :audits, force: true do |t|
3 | t.column :auditable_id, :integer
4 | t.column :auditable_type, :string
5 | t.column :user_id, :integer
6 | t.column :user_type, :string
7 | t.column :username, :string
8 | t.column :action, :string
9 | t.column :audited_changes, :text
10 | t.column :version, :integer, default: 0
11 | t.column :comment, :string
12 | t.column :created_at, :datetime
13 | t.column :remote_address, :string
14 | end
15 |
16 | add_index :audits, [:auditable_id, :auditable_type], name: 'auditable_index'
17 | add_index :audits, [:user_id, :user_type], name: 'user_index'
18 | add_index :audits, :created_at
19 | end
20 |
21 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/add_version_to_auditable_index.rb:
--------------------------------------------------------------------------------
1 | class <%= migration_class_name %> < <%= migration_parent %>
2 | def self.up
3 | if index_exists?(:audits, [:auditable_type, :auditable_id], name: index_name)
4 | remove_index :audits, name: index_name
5 | add_index :audits, [:auditable_type, :auditable_id, :version], name: index_name
6 | end
7 | end
8 |
9 | def self.down
10 | if index_exists?(:audits, [:auditable_type, :auditable_id, :version], name: index_name)
11 | remove_index :audits, name: index_name
12 | add_index :audits, [:auditable_type, :auditable_id], name: index_name
13 | end
14 | end
15 |
16 | private
17 |
18 | def index_name
19 | 'auditable_index'
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/audited.rb:
--------------------------------------------------------------------------------
1 | require 'active_record'
2 |
3 | module Audited
4 | class << self
5 | attr_accessor :ignored_attributes, :current_user_method, :max_audits, :auditing_enabled
6 | attr_writer :audit_class
7 |
8 | def audit_class
9 | @audit_class ||= Audit
10 | end
11 |
12 | def store
13 | Thread.current[:audited_store] ||= {}
14 | end
15 |
16 | def config
17 | yield(self)
18 | end
19 | end
20 |
21 | @ignored_attributes = %w(lock_version created_at updated_at created_on updated_on)
22 |
23 | @current_user_method = :current_user
24 | @auditing_enabled = true
25 | end
26 |
27 | require 'audited/auditor'
28 | require 'audited/audit'
29 |
30 | ::ActiveRecord::Base.send :include, Audited::Auditor
31 |
32 | require 'audited/sweeper'
33 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/revert_polymorphic_indexes_order.rb:
--------------------------------------------------------------------------------
1 | class <%= migration_class_name %> < <%= migration_parent %>
2 | def self.up
3 | fix_index_order_for [:associated_id, :associated_type], 'associated_index'
4 | fix_index_order_for [:auditable_id, :auditable_type], 'auditable_index'
5 | end
6 |
7 | def self.down
8 | fix_index_order_for [:associated_type, :associated_id], 'associated_index'
9 | fix_index_order_for [:auditable_type, :auditable_id], 'auditable_index'
10 | end
11 |
12 | private
13 |
14 | def fix_index_order_for(columns, index_name)
15 | if index_exists? :audits, columns, name: index_name
16 | remove_index :audits, name: index_name
17 | add_index :audits, columns.reverse, name: index_name
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | ENV['RAILS_ENV'] = 'test'
2 | require 'bundler/setup'
3 | require 'single_cov'
4 | SingleCov.setup :rspec
5 |
6 | if Bundler.definition.dependencies.map(&:name).include?('protected_attributes')
7 | require 'protected_attributes'
8 | end
9 | require 'rails_app/config/environment'
10 | require 'rspec/rails'
11 | require 'audited'
12 | require 'audited-rspec'
13 | require 'audited_spec_helpers'
14 | require 'support/active_record/models'
15 |
16 | SPEC_ROOT = Pathname.new(File.expand_path('../', __FILE__))
17 |
18 | Dir[SPEC_ROOT.join('support/*.rb')].each{|f| require f }
19 |
20 | RSpec.configure do |config|
21 | config.include AuditedSpecHelpers
22 | config.use_transactional_fixtures = false if Rails.version.start_with?('4.')
23 | config.use_transactional_tests = false if config.respond_to?(:use_transactional_tests=)
24 | end
25 |
--------------------------------------------------------------------------------
/spec/rails_app/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | RailsApp::Application.configure do
2 | # Settings specified here will take precedence over those in config/environment.rb
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the webserver when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Log error messages when you accidentally call methods on nil.
10 | # config.whiny_nils = true
11 |
12 | # Show full error reports and disable caching
13 | config.consider_all_requests_local = true
14 | config.action_view.debug_rjs = true
15 | config.action_controller.perform_caching = false
16 |
17 | # Don't care if the mailer can't send
18 | config.action_mailer.raise_delivery_errors = false
19 |
20 | config.eager_load = false
21 | end
22 |
--------------------------------------------------------------------------------
/lib/generators/audited/install_generator.rb:
--------------------------------------------------------------------------------
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 | require 'generators/audited/migration_helper'
7 |
8 | module Audited
9 | module Generators
10 | class InstallGenerator < Rails::Generators::Base
11 | include Rails::Generators::Migration
12 | include Audited::Generators::MigrationHelper
13 | extend Audited::Generators::Migration
14 |
15 | class_option :audited_changes_column_type, type: :string, default: "text", required: false
16 | class_option :audited_user_id_column_type, type: :string, default: "integer", required: false
17 |
18 | source_root File.expand_path("../templates", __FILE__)
19 |
20 | def copy_migration
21 | migration_template 'install.rb', 'db/migrate/install_audited.rb'
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/rename_association_to_associated.rb:
--------------------------------------------------------------------------------
1 | class <%= migration_class_name %> < <%= migration_parent %>
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
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright © 2010 Brandon Keepers - Collective Idea
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | # Include DB adapters matching the version requirements in
2 | # rails/activerecord/lib/active_record/connection_adapters/*adapter.rb
3 |
4 | appraise 'rails42' do
5 | gem 'rails', '~> 4.2.0'
6 | gem 'protected_attributes'
7 | gem "mysql2", ">= 0.3.13", "< 0.6.0"
8 | gem "pg", "~> 0.15"
9 | gem "sqlite3", "~> 1.3.6"
10 | end
11 |
12 | appraise 'rails50' do
13 | gem 'rails', '~> 5.0.0'
14 | gem "mysql2", ">= 0.3.18", "< 0.6.0"
15 | gem "pg", ">= 0.18", "< 2.0"
16 | gem "sqlite3", "~> 1.3.6"
17 | end
18 |
19 | appraise 'rails51' do
20 | gem 'rails', '~> 5.1.4'
21 | gem "mysql2", ">= 0.3.18", "< 0.6.0"
22 | gem "pg", ">= 0.18", "< 2.0"
23 | gem "sqlite3", "~> 1.3.6"
24 | end
25 |
26 | appraise 'rails52' do
27 | gem 'rails', '>= 5.2.0', '< 5.3'
28 | gem "mysql2", ">= 0.4.4", "< 0.6.0"
29 | gem "pg", ">= 0.18", "< 2.0"
30 | gem "sqlite3", "~> 1.3.6"
31 | end
32 |
33 | appraise 'rails60' do
34 | gem 'rails', '>= 6.0.0.rc1', '< 6.1'
35 | gem "mysql2", ">= 0.4.4"
36 | gem "pg", ">= 0.18", "< 2.0"
37 | gem "sqlite3", "~> 1.4"
38 | end
39 |
40 | appraise 'rails61' do
41 | gem 'rails', '>= 6.1.0', '< 6.2'
42 | gem "mysql2", ">= 0.4.4"
43 | gem "pg", ">= 1.1", "< 2.0"
44 | gem "sqlite3", "~> 1.4"
45 | end
46 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/install.rb:
--------------------------------------------------------------------------------
1 | class <%= migration_class_name %> < <%= migration_parent %>
2 | def self.up
3 | create_table :audits, :force => true do |t|
4 | t.column :auditable_id, :integer
5 | t.column :auditable_type, :string
6 | t.column :associated_id, :integer
7 | t.column :associated_type, :string
8 | t.column :user_id, :<%= options[:audited_user_id_column_type] %>
9 | t.column :user_type, :string
10 | t.column :username, :string
11 | t.column :action, :string
12 | t.column :audited_changes, :<%= options[:audited_changes_column_type] %>
13 | t.column :version, :integer, :default => 0
14 | t.column :comment, :string
15 | t.column :remote_address, :string
16 | t.column :request_uuid, :string
17 | t.column :created_at, :datetime
18 | end
19 |
20 | add_index :audits, [:auditable_type, :auditable_id, :version], :name => 'auditable_index'
21 | add_index :audits, [:associated_type, :associated_id], :name => 'associated_index'
22 | add_index :audits, [:user_id, :user_type], :name => 'user_index'
23 | add_index :audits, :request_uuid
24 | add_index :audits, :created_at
25 | end
26 |
27 | def self.down
28 | drop_table :audits
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/audited_spec_helpers.rb:
--------------------------------------------------------------------------------
1 | module AuditedSpecHelpers
2 |
3 | def create_user(attrs = {})
4 | Models::ActiveRecord::User.create({name: 'Brandon', username: 'brandon', password: 'password', favourite_device: 'Android Phone'}.merge(attrs))
5 | end
6 |
7 | def build_user(attrs = {})
8 | Models::ActiveRecord::User.new({name: 'darth', username: 'darth', password: 'noooooooo'}.merge(attrs))
9 | end
10 |
11 | def create_versions(n = 2, attrs = {})
12 | Models::ActiveRecord::User.create(name: 'Foobar 1', **attrs).tap do |u|
13 | (n - 1).times do |i|
14 | u.update_attribute :name, "Foobar #{i + 2}"
15 | end
16 | u.reload
17 | end
18 | end
19 |
20 | def run_migrations(direction, migrations_paths, target_version = nil)
21 | if rails_below?('5.2.0.rc1')
22 | ActiveRecord::Migrator.send(direction, migrations_paths, target_version)
23 | elsif rails_below?('6.0.0.rc1')
24 | ActiveRecord::MigrationContext.new(migrations_paths).send(direction, target_version)
25 | else
26 | ActiveRecord::MigrationContext.new(migrations_paths, ActiveRecord::SchemaMigration).send(direction, target_version)
27 | end
28 | end
29 |
30 | def rails_below?(rails_version)
31 | Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new(rails_version)
32 | end
33 |
34 | end
35 |
--------------------------------------------------------------------------------
/spec/rails_app/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | RailsApp::Application.configure do
2 | # Settings specified here will take precedence over those in config/environment.rb
3 |
4 | # The production environment is meant for finished, "live" apps.
5 | # Code is not reloaded between requests
6 | config.cache_classes = true
7 |
8 | # Full error reports are disabled and caching is turned on
9 | config.consider_all_requests_local = false
10 | config.action_controller.perform_caching = true
11 |
12 | # See everything in the log (default is :info)
13 | # config.log_level = :debug
14 |
15 | # Use a different logger for distributed setups
16 | # config.logger = SyslogLogger.new
17 |
18 | # Use a different cache store in production
19 | # config.cache_store = :mem_cache_store
20 |
21 | # Disable Rails's static asset server
22 | # In production, Apache or nginx will already do this
23 | config.serve_static_assets = false
24 |
25 | # Enable serving of images, stylesheets, and javascripts from an asset server
26 | # config.action_controller.asset_host = "http://assets.example.com"
27 |
28 | # Disable delivery errors, bad email addresses will be ignored
29 | # config.action_mailer.raise_delivery_errors = false
30 |
31 | # Enable threaded mode
32 | # config.threadsafe!
33 |
34 | config.eager_load = true
35 | end
36 |
--------------------------------------------------------------------------------
/lib/audited/sweeper.rb:
--------------------------------------------------------------------------------
1 | module Audited
2 | class Sweeper
3 | STORED_DATA = {
4 | current_remote_address: :remote_ip,
5 | current_request_uuid: :request_uuid,
6 | current_user: :current_user
7 | }
8 |
9 | delegate :store, to: ::Audited
10 |
11 | def around(controller)
12 | self.controller = controller
13 | STORED_DATA.each { |k,m| store[k] = send(m) }
14 | yield
15 | ensure
16 | self.controller = nil
17 | STORED_DATA.keys.each { |k| store.delete(k) }
18 | end
19 |
20 | def current_user
21 | lambda { controller.send(Audited.current_user_method) if controller.respond_to?(Audited.current_user_method, true) }
22 | end
23 |
24 | def remote_ip
25 | controller.try(:request).try(:remote_ip)
26 | end
27 |
28 | def request_uuid
29 | controller.try(:request).try(:uuid)
30 | end
31 |
32 | def controller
33 | store[:current_controller]
34 | end
35 |
36 | def controller=(value)
37 | store[:current_controller] = value
38 | end
39 | end
40 | end
41 |
42 | ActiveSupport.on_load(:action_controller) do
43 | if defined?(ActionController::Base)
44 | ActionController::Base.around_action Audited::Sweeper.new
45 | end
46 | if defined?(ActionController::API)
47 | ActionController::API.around_action Audited::Sweeper.new
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/audited.gemspec:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | $:.push File.expand_path("../lib", __FILE__)
3 | require "audited/version"
4 |
5 | Gem::Specification.new do |gem|
6 | gem.name = 'audited'
7 | gem.version = Audited::VERSION
8 |
9 | gem.authors = ['Brandon Keepers', 'Kenneth Kalmer', 'Daniel Morrison', 'Brian Ryckbost', 'Steve Richert', 'Ryan Glover']
10 | gem.email = 'info@collectiveidea.com'
11 | gem.description = 'Log all changes to your models'
12 | gem.summary = gem.description
13 | gem.homepage = 'https://github.com/collectiveidea/audited'
14 | gem.license = 'MIT'
15 |
16 | gem.files = `git ls-files`.split($\).reject{|f| f =~ /(\.gemspec)/ }
17 |
18 | gem.required_ruby_version = '>= 2.3.0'
19 |
20 | gem.add_dependency 'activerecord', '>= 4.2', '< 6.2'
21 |
22 | gem.add_development_dependency 'appraisal'
23 | gem.add_development_dependency 'rails', '>= 4.2', '< 6.2'
24 | gem.add_development_dependency 'rubocop', '~> 0.54.0'
25 | gem.add_development_dependency 'rspec-rails', '~> 3.5'
26 | gem.add_development_dependency 'single_cov'
27 |
28 | # JRuby support for the test ENV
29 | if defined?(JRUBY_VERSION)
30 | gem.add_development_dependency 'activerecord-jdbcsqlite3-adapter', '~> 1.3'
31 | gem.add_development_dependency 'activerecord-jdbcpostgresql-adapter', '~> 1.3'
32 | gem.add_development_dependency 'activerecord-jdbcmysql-adapter', '~> 1.3'
33 | else
34 | gem.add_development_dependency 'sqlite3', '~> 1.3'
35 | gem.add_development_dependency 'mysql2', '>= 0.3.20'
36 | gem.add_development_dependency 'pg', '>= 0.18', '< 2.0'
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | cache: bundler
3 | rvm:
4 | - 2.3.7
5 | - 2.4.4
6 | - 2.5.1
7 | - 2.6.3
8 | - ruby-head
9 | env:
10 | - DB=SQLITE
11 | - DB=POSTGRES
12 | - DB=MYSQL
13 | addons:
14 | postgresql: "9.4"
15 | services:
16 | - mysql
17 | before_install:
18 | # https://github.com/travis-ci/travis-ci/issues/8978
19 | - "travis_retry gem update --system"
20 | # Rails 4.2 has a bundler 1.x requirement
21 | - if [ $BUNDLE_GEMFILE = $PWD/gemfiles/rails42.gemfile ]; then
22 | travis_retry gem install -v '1.17.3' bundler;
23 | bundle _1.17.3_ install;
24 | else
25 | travis_retry gem install bundler;
26 | fi
27 | gemfile:
28 | - gemfiles/rails42.gemfile
29 | - gemfiles/rails50.gemfile
30 | - gemfiles/rails51.gemfile
31 | - gemfiles/rails52.gemfile
32 | - gemfiles/rails60.gemfile
33 | - gemfiles/rails61.gemfile
34 | matrix:
35 | include:
36 | - rvm: 2.6.3
37 | script: bundle exec rubocop --parallel
38 | env: DB=rubocop # make travis build display nicer
39 | exclude:
40 | - rvm: 2.3.7
41 | gemfile: gemfiles/rails61.gemfile
42 | - rvm: 2.4.4
43 | gemfile: gemfiles/rails61.gemfile
44 | - rvm: 2.3.7
45 | gemfile: gemfiles/rails60.gemfile
46 | - rvm: 2.4.4
47 | gemfile: gemfiles/rails60.gemfile
48 | - rvm: 2.6.3
49 | gemfile: gemfiles/rails42.gemfile
50 | - rvm: ruby-head
51 | gemfile: gemfiles/rails42.gemfile
52 | allow_failures:
53 | - rvm: ruby-head
54 | fast_finish: true
55 | branches:
56 | only:
57 | - master
58 | - /.*-stable$/
59 | notifications:
60 | webhooks:
61 | urls:
62 | - http://buildlight.collectiveidea.com/
63 | on_start: always
64 |
--------------------------------------------------------------------------------
/spec/rails_app/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | RailsApp::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure static file server for tests with Cache-Control for performance.
16 | if config.respond_to?(:public_file_server)
17 | config.public_file_server.enabled = true
18 | config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' }
19 | else
20 | config.static_cache_control = 'public, max-age=3600'
21 | config.serve_static_files = true
22 | end
23 |
24 | # Show full error reports and disable caching.
25 | config.consider_all_requests_local = true
26 | # config.action_controller.perform_caching = false
27 |
28 | # Raise exceptions instead of rendering exception templates.
29 | config.action_dispatch.show_exceptions = false
30 |
31 | # Disable request forgery protection in test environment.
32 | # config.action_controller.allow_forgery_protection = false
33 |
34 | # Tell Action Mailer not to deliver emails to the real world.
35 | # The :test delivery method accumulates sent emails in the
36 | # ActionMailer::Base.deliveries array.
37 | config.action_mailer.delivery_method = :test
38 |
39 | # Randomize the order test cases are executed.
40 | config.active_support.test_order = :random
41 |
42 | # Print deprecation notices to the stderr.
43 | config.active_support.deprecation = :stderr
44 |
45 | # Raises error for missing translations
46 | # config.action_view.raise_on_missing_translations = true
47 | end
48 |
--------------------------------------------------------------------------------
/lib/generators/audited/upgrade_generator.rb:
--------------------------------------------------------------------------------
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 | require 'generators/audited/migration_helper'
7 |
8 | module Audited
9 | module Generators
10 | class UpgradeGenerator < Rails::Generators::Base
11 | include Rails::Generators::Migration
12 | include Audited::Generators::MigrationHelper
13 | extend Audited::Generators::Migration
14 |
15 | source_root File.expand_path("../templates", __FILE__)
16 |
17 | def copy_templates
18 | migrations_to_be_applied do |m|
19 | migration_template "#{m}.rb", "db/migrate/#{m}.rb"
20 | end
21 | end
22 |
23 | private
24 |
25 | def migrations_to_be_applied
26 | Audited::Audit.reset_column_information
27 | columns = Audited::Audit.columns.map(&:name)
28 | indexes = Audited::Audit.connection.indexes(Audited::Audit.table_name)
29 |
30 | yield :add_comment_to_audits unless columns.include?('comment')
31 |
32 | if columns.include?('changes')
33 | yield :rename_changes_to_audited_changes
34 | end
35 |
36 | unless columns.include?('remote_address')
37 | yield :add_remote_address_to_audits
38 | end
39 |
40 | unless columns.include?('request_uuid')
41 | yield :add_request_uuid_to_audits
42 | end
43 |
44 | unless columns.include?('association_id')
45 | if columns.include?('auditable_parent_id')
46 | yield :rename_parent_to_association
47 | else
48 | unless columns.include?('associated_id')
49 | yield :add_association_to_audits
50 | end
51 | end
52 | end
53 |
54 | if columns.include?('association_id')
55 | yield :rename_association_to_associated
56 | end
57 |
58 | if indexes.any? { |i| i.columns == %w[associated_id associated_type] }
59 | yield :revert_polymorphic_indexes_order
60 | end
61 |
62 | if indexes.any? { |i| i.columns == %w[auditable_type auditable_id] }
63 | yield :add_version_to_auditable_index
64 | end
65 | end
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/test/install_generator_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | require 'generators/audited/install_generator'
4 |
5 | class InstallGeneratorTest < Rails::Generators::TestCase
6 | destination File.expand_path('../../tmp', __FILE__)
7 | setup :prepare_destination
8 | tests Audited::Generators::InstallGenerator
9 |
10 | test "generate migration with 'text' type for audited_changes column" do
11 | run_generator
12 |
13 | assert_migration "db/migrate/install_audited.rb" do |content|
14 | assert_includes(content, 'class InstallAudited')
15 | assert_includes(content, 't.column :audited_changes, :text')
16 | end
17 | end
18 |
19 | test "generate migration with 'jsonb' type for audited_changes column" do
20 | run_generator %w(--audited-changes-column-type jsonb)
21 |
22 | assert_migration "db/migrate/install_audited.rb" do |content|
23 | assert_includes(content, 'class InstallAudited')
24 | assert_includes(content, 't.column :audited_changes, :jsonb')
25 | end
26 | end
27 |
28 | test "generate migration with 'json' type for audited_changes column" do
29 | run_generator %w(--audited-changes-column-type json)
30 |
31 | assert_migration "db/migrate/install_audited.rb" do |content|
32 | assert_includes(content, 'class InstallAudited')
33 | assert_includes(content, 't.column :audited_changes, :json')
34 | end
35 | end
36 |
37 | test "generate migration with 'string' type for user_id column" do
38 | run_generator %w(--audited-user-id-column-type string)
39 |
40 | assert_migration "db/migrate/install_audited.rb" do |content|
41 | assert_includes(content, 'class InstallAudited')
42 | assert_includes(content, 't.column :user_id, :string')
43 | end
44 | end
45 |
46 | test "generate migration with 'uuid' type for user_id column" do
47 | run_generator %w(--audited-user-id-column-type uuid)
48 |
49 | assert_migration "db/migrate/install_audited.rb" do |content|
50 | assert_includes(content, 'class InstallAudited')
51 | assert_includes(content, 't.column :user_id, :uuid')
52 | end
53 | end
54 |
55 | test "generate migration with correct AR migration parent" do
56 | run_generator
57 |
58 | assert_migration "db/migrate/install_audited.rb" do |content|
59 | parent = Rails::VERSION::MAJOR == 4 ? 'ActiveRecord::Migration' : "ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]"
60 | assert_includes(content, "class InstallAudited < #{parent}\n")
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/spec/support/active_record/schema.rb:
--------------------------------------------------------------------------------
1 | require 'active_record'
2 | require 'logger'
3 |
4 | begin
5 | db_config = ActiveRecord::Base.configurations[Rails.env].clone
6 | db_type = db_config['adapter']
7 | db_name = db_config.delete('database')
8 | raise Exception.new('No database name specified.') if db_name.blank?
9 | if db_type == 'sqlite3'
10 | db_file = Pathname.new(__FILE__).dirname.join(db_name)
11 | db_file.unlink if db_file.file?
12 | else
13 | if defined?(JRUBY_VERSION)
14 | db_config.symbolize_keys!
15 | db_config[:configure_connection] = false
16 | end
17 | adapter = ActiveRecord::Base.send("#{db_type}_connection", db_config)
18 | adapter.recreate_database db_name, db_config.slice('charset').symbolize_keys
19 | adapter.disconnect!
20 | end
21 | rescue => e
22 | Kernel.warn e
23 | end
24 |
25 | logfile = Pathname.new(__FILE__).dirname.join('debug.log')
26 | logfile.unlink if logfile.file?
27 | ActiveRecord::Base.logger = Logger.new(logfile)
28 |
29 | ActiveRecord::Migration.verbose = false
30 | ActiveRecord::Base.establish_connection
31 |
32 | ActiveRecord::Schema.define do
33 | create_table :users do |t|
34 | t.column :name, :string
35 | t.column :username, :string
36 | t.column :password, :string
37 | t.column :activated, :boolean
38 | t.column :status, :integer, default: 0
39 | t.column :suspended_at, :datetime
40 | t.column :logins, :integer, default: 0
41 | t.column :created_at, :datetime
42 | t.column :updated_at, :datetime
43 | t.column :favourite_device, :string
44 | t.column :ssn, :integer
45 | end
46 |
47 | create_table :companies do |t|
48 | t.column :name, :string
49 | t.column :owner_id, :integer
50 | t.column :type, :string
51 | end
52 |
53 | create_table :authors do |t|
54 | t.column :name, :string
55 | end
56 |
57 | create_table :books do |t|
58 | t.column :authord_id, :integer
59 | t.column :title, :string
60 | end
61 |
62 | create_table :audits do |t|
63 | t.column :auditable_id, :integer
64 | t.column :auditable_type, :string
65 | t.column :associated_id, :integer
66 | t.column :associated_type, :string
67 | t.column :user_id, :integer
68 | t.column :user_type, :string
69 | t.column :username, :string
70 | t.column :action, :string
71 | t.column :audited_changes, :text
72 | t.column :version, :integer, default: 0
73 | t.column :comment, :string
74 | t.column :remote_address, :string
75 | t.column :request_uuid, :string
76 | t.column :created_at, :datetime
77 | end
78 |
79 | add_index :audits, [:auditable_id, :auditable_type], name: 'auditable_index'
80 | add_index :audits, [:associated_id, :associated_type], name: 'associated_index'
81 | add_index :audits, [:user_id, :user_type], name: 'user_index'
82 | add_index :audits, :request_uuid
83 | add_index :audits, :created_at
84 | end
85 |
--------------------------------------------------------------------------------
/spec/audited/rspec_matchers_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Models::ActiveRecord::UserExceptPassword do
4 | let(:non_audited_columns) { subject.class.non_audited_columns }
5 |
6 | it { should_not be_audited.only(non_audited_columns) }
7 | it { should be_audited.except(:password) }
8 | it { should_not be_audited.requires_comment }
9 | it { should be_audited.on(:create, :update, :destroy) }
10 | # test chaining
11 | it { should be_audited.except(:password).on(:create, :update, :destroy) }
12 | end
13 |
14 | describe Models::ActiveRecord::UserOnlyPassword do
15 | let(:audited_columns) { subject.class.audited_columns }
16 |
17 | it { should be_audited.only(:password) }
18 | it { should_not be_audited.except(audited_columns) }
19 | it { should_not be_audited.requires_comment }
20 | it { should be_audited.on(:create, :update, :destroy) }
21 | it { should be_audited.only(:password).on(:create, :update, :destroy) }
22 | end
23 |
24 | describe Models::ActiveRecord::CommentRequiredUser do
25 | let(:audited_columns) { subject.class.audited_columns }
26 | let(:non_audited_columns) { subject.class.non_audited_columns }
27 |
28 | it { should_not be_audited.only(non_audited_columns) }
29 | it { should_not be_audited.except(audited_columns) }
30 | it { should be_audited.requires_comment }
31 | it { should be_audited.on(:create, :update, :destroy) }
32 | it { should be_audited.requires_comment.on(:create, :update, :destroy) }
33 | end
34 |
35 | describe Models::ActiveRecord::OnCreateCommentRequiredUser do
36 | let(:audited_columns) { subject.class.audited_columns }
37 | let(:non_audited_columns) { subject.class.non_audited_columns }
38 |
39 | it { should_not be_audited.only(non_audited_columns) }
40 | it { should_not be_audited.except(audited_columns) }
41 | it { should be_audited.requires_comment }
42 | it { should be_audited.on(:create) }
43 | it { should_not be_audited.on(:update, :destroy) }
44 | it { should be_audited.requires_comment.on(:create) }
45 | end
46 |
47 | describe Models::ActiveRecord::OnUpdateCommentRequiredUser do
48 | let(:audited_columns) { subject.class.audited_columns }
49 | let(:non_audited_columns) { subject.class.non_audited_columns }
50 |
51 | it { should_not be_audited.only(non_audited_columns) }
52 | it { should_not be_audited.except(audited_columns) }
53 | it { should be_audited.requires_comment }
54 | it { should be_audited.on(:update) }
55 | it { should_not be_audited.on(:create, :destroy) }
56 | it { should be_audited.requires_comment.on(:update) }
57 | end
58 |
59 | describe Models::ActiveRecord::OnDestroyCommentRequiredUser do
60 | let(:audited_columns) { subject.class.audited_columns }
61 | let(:non_audited_columns) { subject.class.non_audited_columns }
62 |
63 | it { should_not be_audited.only(non_audited_columns) }
64 | it { should_not be_audited.except(audited_columns) }
65 | it { should be_audited.requires_comment }
66 | it { should be_audited.on(:destroy) }
67 | it { should_not be_audited.on(:create, :update) }
68 | it { should be_audited.requires_comment.on(:destroy) }
69 | end
70 |
--------------------------------------------------------------------------------
/test/upgrade_generator_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | require 'generators/audited/upgrade_generator'
4 |
5 | class UpgradeGeneratorTest < Rails::Generators::TestCase
6 | destination File.expand_path('../../tmp', __FILE__)
7 | setup :prepare_destination
8 | tests Audited::Generators::UpgradeGenerator
9 | if Rails::VERSION::MAJOR == 4
10 | self.use_transactional_fixtures = false
11 | else
12 | self.use_transactional_tests = false
13 | end
14 |
15 | test "should add 'comment' to audits table" do
16 | load_schema 1
17 |
18 | run_generator %w(upgrade)
19 |
20 | assert_migration "db/migrate/add_comment_to_audits.rb" do |content|
21 | assert_match(/add_column :audits, :comment, :string/, content)
22 | end
23 |
24 | assert_migration "db/migrate/rename_changes_to_audited_changes.rb"
25 | end
26 |
27 | test "should rename 'changes' to 'audited_changes'" do
28 | load_schema 2
29 |
30 | run_generator %w(upgrade)
31 |
32 | assert_no_migration "db/migrate/add_comment_to_audits.rb"
33 |
34 | assert_migration "db/migrate/rename_changes_to_audited_changes.rb" do |content|
35 | assert_match(/rename_column :audits, :changes, :audited_changes/, content)
36 | end
37 | end
38 |
39 | test "should add a 'remote_address' to audits table" do
40 | load_schema 3
41 |
42 | run_generator %w(upgrade)
43 |
44 | assert_migration "db/migrate/add_remote_address_to_audits.rb" do |content|
45 | assert_match(/add_column :audits, :remote_address, :string/, content)
46 | end
47 | end
48 |
49 | test "should add 'association_id' and 'association_type' to audits table" do
50 | load_schema 4
51 |
52 | run_generator %w(upgrade)
53 |
54 | assert_migration "db/migrate/add_association_to_audits.rb" do |content|
55 | assert_match(/add_column :audits, :association_id, :integer/, content)
56 | assert_match(/add_column :audits, :association_type, :string/, content)
57 | end
58 | end
59 |
60 | test "should rename 'association_id' to 'associated_id' and 'association_type' to 'associated_type'" do
61 | load_schema 5
62 |
63 | run_generator %w(upgrade)
64 |
65 | assert_migration "db/migrate/rename_association_to_associated.rb" do |content|
66 | assert_match(/rename_column :audits, :association_id, :associated_id/, content)
67 | assert_match(/rename_column :audits, :association_type, :associated_type/, content)
68 | end
69 | end
70 |
71 | test "should add 'request_uuid' to audits table" do
72 | load_schema 6
73 |
74 | run_generator %w(upgrade)
75 |
76 | assert_migration "db/migrate/add_request_uuid_to_audits.rb" do |content|
77 | assert_match(/add_column :audits, :request_uuid, :string/, content)
78 | assert_match(/add_index :audits, :request_uuid/, content)
79 | end
80 | end
81 |
82 | test "should add 'version' to auditable_index" do
83 | load_schema 6
84 |
85 | run_generator %w(upgrade)
86 |
87 | assert_migration "db/migrate/add_version_to_auditable_index.rb" do |content|
88 | assert_match(/add_index :audits, \[:auditable_type, :auditable_id, :version\]/, content)
89 | end
90 | end
91 |
92 | test "generate migration with correct AR migration parent" do
93 | load_schema 1
94 |
95 | run_generator %w(upgrade)
96 |
97 | assert_migration "db/migrate/add_comment_to_audits.rb" do |content|
98 | parent = Rails::VERSION::MAJOR == 4 ? 'ActiveRecord::Migration' : "ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]"
99 | assert_includes(content, "class AddCommentToAudits < #{parent}\n")
100 | end
101 | end
102 | end
103 |
--------------------------------------------------------------------------------
/spec/audited/sweeper_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | SingleCov.covered! uncovered: 2 # 2 conditional on_load conditions
4 |
5 | class AuditsController < ActionController::Base
6 | before_action :populate_user
7 |
8 | attr_reader :company
9 |
10 | def create
11 | @company = Models::ActiveRecord::Company.create
12 | head :ok
13 | end
14 |
15 | def update
16 | current_user.update!(password: 'foo')
17 | head :ok
18 | end
19 |
20 | private
21 |
22 | attr_accessor :current_user
23 | attr_accessor :custom_user
24 |
25 | def populate_user; end
26 | end
27 |
28 | describe AuditsController do
29 | include RSpec::Rails::ControllerExampleGroup
30 | render_views
31 |
32 | before do
33 | Audited.current_user_method = :current_user
34 | end
35 |
36 | let(:user) { create_user }
37 |
38 | describe "POST audit" do
39 | it "should audit user" do
40 | controller.send(:current_user=, user)
41 | expect {
42 | post :create
43 | }.to change( Audited::Audit, :count )
44 |
45 | expect(controller.company.audits.last.user).to eq(user)
46 | end
47 |
48 | it "does not audit when method is not found" do
49 | controller.send(:current_user=, user)
50 | Audited.current_user_method = :nope
51 | expect {
52 | post :create
53 | }.to change( Audited::Audit, :count )
54 | expect(controller.company.audits.last.user).to eq(nil)
55 | end
56 |
57 | it "should support custom users for sweepers" do
58 | controller.send(:custom_user=, user)
59 | Audited.current_user_method = :custom_user
60 |
61 | expect {
62 | post :create
63 | }.to change( Audited::Audit, :count )
64 |
65 | expect(controller.company.audits.last.user).to eq(user)
66 | end
67 |
68 | it "should record the remote address responsible for the change" do
69 | request.env['REMOTE_ADDR'] = "1.2.3.4"
70 | controller.send(:current_user=, user)
71 |
72 | post :create
73 |
74 | expect(controller.company.audits.last.remote_address).to eq('1.2.3.4')
75 | end
76 |
77 | it "should record a UUID for the web request responsible for the change" do
78 | allow_any_instance_of(ActionDispatch::Request).to receive(:uuid).and_return("abc123")
79 | controller.send(:current_user=, user)
80 |
81 | post :create
82 |
83 | expect(controller.company.audits.last.request_uuid).to eq("abc123")
84 | end
85 |
86 | it "should call current_user after controller callbacks" do
87 | expect(controller).to receive(:populate_user) do
88 | controller.send(:current_user=, user)
89 | end
90 |
91 | expect {
92 | post :create
93 | }.to change( Audited::Audit, :count )
94 |
95 | expect(controller.company.audits.last.user).to eq(user)
96 | end
97 | end
98 |
99 | describe "PUT update" do
100 | it "should not save blank audits" do
101 | controller.send(:current_user=, user)
102 |
103 | expect {
104 | params = Rails::VERSION::MAJOR == 4 ? {id: 123} : {params: {id: 123}}
105 | put :update, **params
106 | }.to_not change( Audited::Audit, :count )
107 | end
108 | end
109 | end
110 |
111 | describe Audited::Sweeper do
112 |
113 | it "should be thread-safe" do
114 | instance = Audited::Sweeper.new
115 |
116 | t1 = Thread.new do
117 | sleep 0.5
118 | instance.controller = 'thread1 controller instance'
119 | expect(instance.controller).to eq('thread1 controller instance')
120 | end
121 |
122 | t2 = Thread.new do
123 | instance.controller = 'thread2 controller instance'
124 | sleep 1
125 | expect(instance.controller).to eq('thread2 controller instance')
126 | end
127 |
128 | t1.join; t2.join
129 |
130 | expect(instance.controller).to be_nil
131 | end
132 |
133 | end
134 |
--------------------------------------------------------------------------------
/spec/support/active_record/models.rb:
--------------------------------------------------------------------------------
1 | require 'cgi'
2 | require File.expand_path('../schema', __FILE__)
3 |
4 | module Models
5 | module ActiveRecord
6 | class User < ::ActiveRecord::Base
7 | audited except: :password
8 | attribute :non_column_attr if Rails.version >= '5.1'
9 | attr_protected :logins if respond_to?(:attr_protected)
10 | enum status: { active: 0, reliable: 1, banned: 2 }
11 |
12 | def name=(val)
13 | write_attribute(:name, CGI.escapeHTML(val))
14 | end
15 | end
16 |
17 | class UserExceptPassword < ::ActiveRecord::Base
18 | self.table_name = :users
19 | audited except: :password
20 | end
21 |
22 | class UserOnlyPassword < ::ActiveRecord::Base
23 | self.table_name = :users
24 | attribute :non_column_attr if Rails.version >= '5.1'
25 | audited only: :password
26 | end
27 |
28 | class UserRedactedPassword < ::ActiveRecord::Base
29 | self.table_name = :users
30 | audited redacted: :password
31 | end
32 |
33 | class UserMultipleRedactedAttributes < ::ActiveRecord::Base
34 | self.table_name = :users
35 | audited redacted: [:password, :ssn]
36 | end
37 |
38 | class UserRedactedPasswordCustomRedaction < ::ActiveRecord::Base
39 | self.table_name = :users
40 | audited redacted: :password, redaction_value: ["My", "Custom", "Value", 7]
41 | end
42 |
43 | class CommentRequiredUser < ::ActiveRecord::Base
44 | self.table_name = :users
45 | audited comment_required: true
46 | end
47 |
48 | class OnCreateCommentRequiredUser < ::ActiveRecord::Base
49 | self.table_name = :users
50 | audited comment_required: true, on: :create
51 | end
52 |
53 | class OnUpdateCommentRequiredUser < ::ActiveRecord::Base
54 | self.table_name = :users
55 | audited comment_required: true, on: :update
56 | end
57 |
58 | class OnDestroyCommentRequiredUser < ::ActiveRecord::Base
59 | self.table_name = :users
60 | audited comment_required: true, on: :destroy
61 | end
62 |
63 | class NoUpdateWithCommentOnlyUser < ::ActiveRecord::Base
64 | self.table_name = :users
65 | audited update_with_comment_only: false
66 | end
67 |
68 | class AccessibleAfterDeclarationUser < ::ActiveRecord::Base
69 | self.table_name = :users
70 | audited
71 | attr_accessible :name, :username, :password if respond_to?(:attr_accessible)
72 | end
73 |
74 | class AccessibleBeforeDeclarationUser < ::ActiveRecord::Base
75 | self.table_name = :users
76 | attr_accessible :name, :username, :password if respond_to?(:attr_accessible) # declare attr_accessible before calling aaa
77 | audited
78 | end
79 |
80 | class NoAttributeProtectionUser < ::ActiveRecord::Base
81 | self.table_name = :users
82 | audited
83 | end
84 |
85 | class UserWithAfterAudit < ::ActiveRecord::Base
86 | self.table_name = :users
87 | audited
88 | attr_accessor :bogus_attr, :around_attr
89 |
90 | private
91 |
92 | def after_audit
93 | self.bogus_attr = "do something"
94 | end
95 |
96 | def around_audit
97 | self.around_attr = yield
98 | end
99 | end
100 |
101 | class MaxAuditsUser < ::ActiveRecord::Base
102 | self.table_name = :users
103 | audited max_audits: 5
104 | end
105 |
106 | class Company < ::ActiveRecord::Base
107 | audited
108 | end
109 |
110 | class Company::STICompany < Company
111 | end
112 |
113 | class Owner < ::ActiveRecord::Base
114 | self.table_name = 'users'
115 | audited
116 | has_associated_audits
117 | has_many :companies, class_name: "OwnedCompany", dependent: :destroy
118 | end
119 |
120 | class OwnedCompany < ::ActiveRecord::Base
121 | self.table_name = 'companies'
122 | belongs_to :owner, class_name: "Owner"
123 | attr_accessible :name, :owner if respond_to?(:attr_accessible) # declare attr_accessible before calling aaa
124 | audited associated_with: :owner
125 | end
126 |
127 | class OnUpdateDestroy < ::ActiveRecord::Base
128 | self.table_name = 'companies'
129 | audited on: [:update, :destroy]
130 | end
131 |
132 | class OnCreateDestroy < ::ActiveRecord::Base
133 | self.table_name = 'companies'
134 | audited on: [:create, :destroy]
135 | end
136 |
137 | class OnCreateDestroyExceptName < ::ActiveRecord::Base
138 | self.table_name = 'companies'
139 | audited except: :name, on: [:create, :destroy]
140 | end
141 |
142 | class OnCreateUpdate < ::ActiveRecord::Base
143 | self.table_name = 'companies'
144 | audited on: [:create, :update]
145 | end
146 | end
147 | end
148 |
--------------------------------------------------------------------------------
/lib/audited/audit.rb:
--------------------------------------------------------------------------------
1 | require 'set'
2 |
3 | module Audited
4 | # Audit saves the changes to ActiveRecord models. It has the following attributes:
5 | #
6 | # * auditable: the ActiveRecord model that was changed
7 | # * user: the user that performed the change; a string or an ActiveRecord model
8 | # * action: one of create, update, or delete
9 | # * audited_changes: a hash of all the changes
10 | # * comment: a comment set with the audit
11 | # * version: the version of the model
12 | # * request_uuid: a uuid based that allows audits from the same controller request
13 | # * created_at: Time that the change was performed
14 | #
15 |
16 | class YAMLIfTextColumnType
17 | class << self
18 | def load(obj)
19 | if Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
20 | ActiveRecord::Coders::YAMLColumn.new(Object).load(obj)
21 | else
22 | obj
23 | end
24 | end
25 |
26 | def dump(obj)
27 | if Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
28 | ActiveRecord::Coders::YAMLColumn.new(Object).dump(obj)
29 | else
30 | obj
31 | end
32 | end
33 | end
34 | end
35 |
36 | class Audit < ::ActiveRecord::Base
37 | belongs_to :auditable, polymorphic: true
38 | belongs_to :user, polymorphic: true
39 | belongs_to :associated, polymorphic: true
40 |
41 | before_create :set_version_number, :set_audit_user, :set_request_uuid, :set_remote_address
42 |
43 | cattr_accessor :audited_class_names
44 | self.audited_class_names = Set.new
45 |
46 | serialize :audited_changes, YAMLIfTextColumnType
47 |
48 | scope :ascending, ->{ reorder(version: :asc) }
49 | scope :descending, ->{ reorder(version: :desc)}
50 | scope :creates, ->{ where(action: 'create')}
51 | scope :updates, ->{ where(action: 'update')}
52 | scope :destroys, ->{ where(action: 'destroy')}
53 |
54 | scope :up_until, ->(date_or_time){ where("created_at <= ?", date_or_time) }
55 | scope :from_version, ->(version){ where('version >= ?', version) }
56 | scope :to_version, ->(version){ where('version <= ?', version) }
57 | scope :auditable_finder, ->(auditable_id, auditable_type){ where(auditable_id: auditable_id, auditable_type: auditable_type)}
58 | # Return all audits older than the current one.
59 | def ancestors
60 | self.class.ascending.auditable_finder(auditable_id, auditable_type).to_version(version)
61 | end
62 |
63 | # Return an instance of what the object looked like at this revision. If
64 | # the object has been destroyed, this will be a new record.
65 | def revision
66 | clazz = auditable_type.constantize
67 | (clazz.find_by_id(auditable_id) || clazz.new).tap do |m|
68 | self.class.assign_revision_attributes(m, self.class.reconstruct_attributes(ancestors).merge(audit_version: version))
69 | end
70 | end
71 |
72 | # Returns a hash of the changed attributes with the new values
73 | def new_attributes
74 | (audited_changes || {}).inject({}.with_indifferent_access) do |attrs, (attr, values)|
75 | attrs[attr] = values.is_a?(Array) ? values.last : values
76 | attrs
77 | end
78 | end
79 |
80 | # Returns a hash of the changed attributes with the old values
81 | def old_attributes
82 | (audited_changes || {}).inject({}.with_indifferent_access) do |attrs, (attr, values)|
83 | attrs[attr] = Array(values).first
84 |
85 | attrs
86 | end
87 | end
88 |
89 | # Allows user to undo changes
90 | def undo
91 | case action
92 | when 'create'
93 | # destroys a newly created record
94 | auditable.destroy!
95 | when 'destroy'
96 | # creates a new record with the destroyed record attributes
97 | auditable_type.constantize.create!(audited_changes)
98 | when 'update'
99 | # changes back attributes
100 | auditable.update!(audited_changes.transform_values(&:first))
101 | else
102 | raise StandardError, "invalid action given #{action}"
103 | end
104 | end
105 |
106 | # Allows user to be set to either a string or an ActiveRecord object
107 | # @private
108 | def user_as_string=(user)
109 | # reset both either way
110 | self.user_as_model = self.username = nil
111 | user.is_a?(::ActiveRecord::Base) ?
112 | self.user_as_model = user :
113 | self.username = user
114 | end
115 | alias_method :user_as_model=, :user=
116 | alias_method :user=, :user_as_string=
117 |
118 | # @private
119 | def user_as_string
120 | user_as_model || username
121 | end
122 | alias_method :user_as_model, :user
123 | alias_method :user, :user_as_string
124 |
125 | # Returns the list of classes that are being audited
126 | def self.audited_classes
127 | audited_class_names.map(&:constantize)
128 | end
129 |
130 | # All audits made during the block called will be recorded as made
131 | # by +user+. This method is hopefully threadsafe, making it ideal
132 | # for background operations that require audit information.
133 | def self.as_user(user)
134 | last_audited_user = ::Audited.store[:audited_user]
135 | ::Audited.store[:audited_user] = user
136 | yield
137 | ensure
138 | ::Audited.store[:audited_user] = last_audited_user
139 | end
140 |
141 | # @private
142 | def self.reconstruct_attributes(audits)
143 | audits.each_with_object({}) do |audit, all|
144 | all.merge!(audit.new_attributes)
145 | all[:audit_version] = audit.version
146 | end
147 | end
148 |
149 | # @private
150 | def self.assign_revision_attributes(record, attributes)
151 | attributes.each do |attr, val|
152 | record = record.dup if record.frozen?
153 |
154 | if record.respond_to?("#{attr}=")
155 | record.attributes.key?(attr.to_s) ?
156 | record[attr] = val :
157 | record.send("#{attr}=", val)
158 | end
159 | end
160 | record
161 | end
162 |
163 | # use created_at as timestamp cache key
164 | def self.collection_cache_key(collection = all, *)
165 | super(collection, :created_at)
166 | end
167 |
168 | private
169 |
170 | def set_version_number
171 | if action == 'create'
172 | self.version = 1
173 | else
174 | collection = Rails::VERSION::MAJOR == 6 ? self.class.unscoped : self.class
175 | max = collection.auditable_finder(auditable_id, auditable_type).maximum(:version) || 0
176 | self.version = max + 1
177 | end
178 | end
179 |
180 | def set_audit_user
181 | self.user ||= ::Audited.store[:audited_user] # from .as_user
182 | self.user ||= ::Audited.store[:current_user].try!(:call) # from Sweeper
183 | nil # prevent stopping callback chains
184 | end
185 |
186 | def set_request_uuid
187 | self.request_uuid ||= ::Audited.store[:current_request_uuid]
188 | self.request_uuid ||= SecureRandom.uuid
189 | end
190 |
191 | def set_remote_address
192 | self.remote_address ||= ::Audited.store[:current_remote_address]
193 | end
194 | end
195 | end
196 |
--------------------------------------------------------------------------------
/lib/audited/rspec_matchers.rb:
--------------------------------------------------------------------------------
1 | module Audited
2 | module RspecMatchers
3 | # Ensure that the model is audited.
4 | #
5 | # Options:
6 | # * associated_with - tests that the audit makes use of the associated_with option
7 | # * only - tests that the audit makes use of the only option *Overrides except option*
8 | # * except - tests that the audit makes use of the except option
9 | # * requires_comment - if specified, then the audit must require comments through the audit_comment attribute
10 | # * on - tests that the audit makes use of the on option with specified parameters
11 | #
12 | # Example:
13 | # it { should be_audited }
14 | # it { should be_audited.associated_with(:user) }
15 | # it { should be_audited.only(:field_name) }
16 | # it { should be_audited.except(:password) }
17 | # it { should be_audited.requires_comment }
18 | # it { should be_audited.on(:create).associated_with(:user).except(:password) }
19 | #
20 | def be_audited
21 | AuditMatcher.new
22 | end
23 |
24 | # Ensure that the model has associated audits
25 | #
26 | # Example:
27 | # it { should have_associated_audits }
28 | #
29 | def have_associated_audits
30 | AssociatedAuditMatcher.new
31 | end
32 |
33 | class AuditMatcher # :nodoc:
34 | def initialize
35 | @options = {}
36 | end
37 |
38 | def associated_with(model)
39 | @options[:associated_with] = model
40 | self
41 | end
42 |
43 | def only(*fields)
44 | @options[:only] = fields.flatten.map(&:to_s)
45 | self
46 | end
47 |
48 | def except(*fields)
49 | @options[:except] = fields.flatten.map(&:to_s)
50 | self
51 | end
52 |
53 | def requires_comment
54 | @options[:comment_required] = true
55 | self
56 | end
57 |
58 | def on(*actions)
59 | @options[:on] = actions.flatten.map(&:to_sym)
60 | self
61 | end
62 |
63 | def matches?(subject)
64 | @subject = subject
65 | auditing_enabled? && required_checks_for_options_satisfied?
66 | end
67 |
68 | def failure_message
69 | "Expected #{@expectation}"
70 | end
71 |
72 | def negative_failure_message
73 | "Did not expect #{@expectation}"
74 | end
75 |
76 | alias_method :failure_message_when_negated, :negative_failure_message
77 |
78 | def description
79 | description = "audited"
80 | description += " associated with #{@options[:associated_with]}" if @options.key?(:associated_with)
81 | description += " only => #{@options[:only].join ', '}" if @options.key?(:only)
82 | description += " except => #{@options[:except].join(', ')}" if @options.key?(:except)
83 | description += " requires audit_comment" if @options.key?(:comment_required)
84 |
85 | description
86 | end
87 |
88 | protected
89 |
90 | def expects(message)
91 | @expectation = message
92 | end
93 |
94 | def auditing_enabled?
95 | expects "#{model_class} to be audited"
96 | model_class.respond_to?(:auditing_enabled) && model_class.auditing_enabled
97 | end
98 |
99 | def model_class
100 | @subject.class
101 | end
102 |
103 | def associated_with_model?
104 | expects "#{model_class} to record audits to associated model #{@options[:associated_with]}"
105 | model_class.audit_associated_with == @options[:associated_with]
106 | end
107 |
108 | def records_changes_to_specified_fields?
109 | ignored_fields = build_ignored_fields_from_options
110 |
111 | expects "non audited columns (#{model_class.non_audited_columns.inspect}) to match (#{ignored_fields})"
112 | model_class.non_audited_columns.to_set == ignored_fields.to_set
113 | end
114 |
115 | def comment_required_valid?
116 | expects "to require audit_comment before #{model_class.audited_options[:on]} when comment required"
117 | validate_callbacks_include_presence_of_comment? && destroy_callbacks_include_comment_required?
118 | end
119 |
120 | def only_audit_on_designated_callbacks?
121 | {
122 | create: [:after, :audit_create],
123 | update: [:before, :audit_update],
124 | destroy: [:before, :audit_destroy]
125 | }.map do |(action, kind_callback)|
126 | kind, callback = kind_callback
127 | callbacks_for(action, kind: kind).include?(callback) if @options[:on].include?(action)
128 | end.compact.all?
129 | end
130 |
131 | def validate_callbacks_include_presence_of_comment?
132 | if @options[:comment_required] && audited_on_create_or_update?
133 | callbacks_for(:validate).include?(:presence_of_audit_comment)
134 | else
135 | true
136 | end
137 | end
138 |
139 | def audited_on_create_or_update?
140 | model_class.audited_options[:on].include?(:create) || model_class.audited_options[:on].include?(:update)
141 | end
142 |
143 | def destroy_callbacks_include_comment_required?
144 | if @options[:comment_required] && model_class.audited_options[:on].include?(:destroy)
145 | callbacks_for(:destroy).include?(:require_comment)
146 | else
147 | true
148 | end
149 | end
150 |
151 | def requires_comment_before_callbacks?
152 | [:create, :update, :destroy].map do |action|
153 | if @options[:comment_required] && model_class.audited_options[:on].include?(action)
154 | callbacks_for(action).include?(:require_comment)
155 | end
156 | end.compact.all?
157 | end
158 |
159 | def callbacks_for(action, kind: :before)
160 | model_class.send("_#{action}_callbacks").select { |cb| cb.kind == kind }.map(&:filter)
161 | end
162 |
163 | def build_ignored_fields_from_options
164 | default_ignored_attributes = model_class.default_ignored_attributes
165 |
166 | if @options[:only].present?
167 | (default_ignored_attributes | model_class.column_names) - @options[:only]
168 | elsif @options[:except].present?
169 | default_ignored_attributes | @options[:except]
170 | else
171 | default_ignored_attributes
172 | end
173 | end
174 |
175 | def required_checks_for_options_satisfied?
176 | {
177 | only: :records_changes_to_specified_fields?,
178 | except: :records_changes_to_specified_fields?,
179 | comment_required: :comment_required_valid?,
180 | associated_with: :associated_with_model?,
181 | on: :only_audit_on_designated_callbacks?
182 | }.map do |(option, check)|
183 | send(check) if @options[option].present?
184 | end.compact.all?
185 | end
186 | end
187 |
188 | class AssociatedAuditMatcher # :nodoc:
189 | def matches?(subject)
190 | @subject = subject
191 |
192 | association_exists?
193 | end
194 |
195 | def failure_message
196 | "Expected #{model_class} to have associated audits"
197 | end
198 |
199 | def negative_failure_message
200 | "Expected #{model_class} to not have associated audits"
201 | end
202 |
203 | alias_method :failure_message_when_negated, :negative_failure_message
204 |
205 | def description
206 | "has associated audits"
207 | end
208 |
209 | protected
210 |
211 | def model_class
212 | @subject.class
213 | end
214 |
215 | def reflection
216 | model_class.reflect_on_association(:associated_audits)
217 | end
218 |
219 | def association_exists?
220 | !reflection.nil? &&
221 | reflection.macro == :has_many &&
222 | reflection.options[:class_name] == Audited.audit_class.name
223 | end
224 | end
225 | end
226 | end
227 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Audited ChangeLog
2 |
3 | ## Unreleased
4 |
5 | ## 4.9.0 (2019-07-17)
6 |
7 | Breaking changes
8 |
9 | - removed block support for `Audit.reconstruct_attributes`
10 | [#437](https://github.com/collectiveidea/audited/pull/437)
11 | - removed `audited_columns`, `non_audited_columns`, `auditing_enabled=` instance methods,
12 | use class methods instead
13 | [#424](https://github.com/collectiveidea/audited/pull/424)
14 | - removed rails 4.1 and 4.0 support
15 | [#431](https://github.com/collectiveidea/audited/pull/431)
16 |
17 | Added
18 |
19 | - Add `with_auditing` methods to enable temporarily
20 | [#502](https://github.com/collectiveidea/audited/pull/502)
21 | - Add `update_with_comment_only` option to control audit creation with only comments
22 | [#327](https://github.com/collectiveidea/audited/pull/327)
23 | - Support for Rails 6.0 and Ruby 2.6
24 | [#494](https://github.com/collectiveidea/audited/pull/494)
25 |
26 | Changed
27 |
28 | - None
29 |
30 | Fixed
31 |
32 | - Ensure enum changes are stored consistently
33 | [#429](https://github.com/collectiveidea/audited/pull/429)
34 |
35 | ## 4.8.0 (2018-08-19)
36 |
37 | Breaking changes
38 |
39 | - None
40 |
41 | Added
42 |
43 | - Add ability to globally disable auditing
44 | [#426](https://github.com/collectiveidea/audited/pull/426)
45 | - Add `own_and_associated_audits` method to auditable models
46 | [#428](https://github.com/collectiveidea/audited/pull/428)
47 | - Ability to nest `as_user` within itself
48 | [#450](https://github.com/collectiveidea/audited/pull/450)
49 | - Private methods can now be used for conditional auditing
50 | [#454](https://github.com/collectiveidea/audited/pull/454)
51 |
52 | Changed
53 |
54 | - Add version to `auditable_index`
55 | [#427](https://github.com/collectiveidea/audited/pull/427)
56 | - Rename audited resource revision `version` attribute to `audit_version` and deprecate `version` attribute
57 | [#443](https://github.com/collectiveidea/audited/pull/443)
58 |
59 | Fixed
60 |
61 | - None
62 |
63 | ## 4.7.1 (2018-04-10)
64 |
65 | Breaking changes
66 |
67 | - None
68 |
69 | Added
70 |
71 | - None
72 |
73 | Changed
74 |
75 | - None
76 |
77 | Fixed
78 |
79 | - Allow use with Rails 5.2 final
80 |
81 | ## 4.7.0 (2018-03-14)
82 |
83 | Breaking changes
84 |
85 | - None
86 |
87 | Added
88 |
89 | - Add `inverse_of: auditable` definition to audit relation
90 | [#413](https://github.com/collectiveidea/audited/pull/413)
91 | - Add functionality to conditionally audit models
92 | [#414](https://github.com/collectiveidea/audited/pull/414)
93 | - Allow limiting number of audits stored
94 | [#405](https://github.com/collectiveidea/audited/pull/405)
95 |
96 | Changed
97 |
98 | - Reduced db calls in `#revisions` method
99 | [#402](https://github.com/collectiveidea/audited/pull/402)
100 | [#403](https://github.com/collectiveidea/audited/pull/403)
101 | - Update supported Ruby and Rails versions
102 | [#404](https://github.com/collectiveidea/audited/pull/404)
103 | [#409](https://github.com/collectiveidea/audited/pull/409)
104 | [#415](https://github.com/collectiveidea/audited/pull/415)
105 | [#416](https://github.com/collectiveidea/audited/pull/416)
106 |
107 | Fixed
108 |
109 | - Ensure that `on` and `except` options jive with `comment_required: true`
110 | [#419](https://github.com/collectiveidea/audited/pull/419)
111 | - Fix RSpec matchers
112 | [#420](https://github.com/collectiveidea/audited/pull/420)
113 |
114 | ## 4.6.0 (2018-01-10)
115 |
116 | Breaking changes
117 |
118 | - None
119 |
120 | Added
121 |
122 | - Add functionality to undo specific audit
123 | [#381](https://github.com/collectiveidea/audited/pull/381)
124 |
125 | Changed
126 |
127 | - Removed duplicate declaration of `non_audited_columns` method
128 | [#365](https://github.com/collectiveidea/audited/pull/365)
129 | - Updated `audited_changes` calculation to support Rails>=5.1 change syntax
130 | [#377](https://github.com/collectiveidea/audited/pull/377)
131 | - Improve index ordering for polymorphic indexes
132 | [#385](https://github.com/collectiveidea/audited/pull/385)
133 | - Update CI to test on newer versions of Ruby and Rails
134 | [#386](https://github.com/collectiveidea/audited/pull/386)
135 | [#387](https://github.com/collectiveidea/audited/pull/387)
136 | [#388](https://github.com/collectiveidea/audited/pull/388)
137 | - Simplify `audited_columns` calculation
138 | [#391](https://github.com/collectiveidea/audited/pull/391)
139 | - Simplify `audited_changes` calculation
140 | [#389](https://github.com/collectiveidea/audited/pull/389)
141 | - Normalize options passed to `audited` method
142 | [#397](https://github.com/collectiveidea/audited/pull/397)
143 |
144 | Fixed
145 |
146 | - Fixed typo in rspec causing incorrect test failure
147 | [#360](https://github.com/collectiveidea/audited/pull/360)
148 | - Allow running specs using rake
149 | [#390](https://github.com/collectiveidea/audited/pull/390)
150 | - Passing an invalid version to `revision` returns `nil` instead of last version
151 | [#384](https://github.com/collectiveidea/audited/pull/384)
152 | - Fix duplicate deceleration warnings
153 | [#399](https://github.com/collectiveidea/audited/pull/399)
154 |
155 |
156 | ## 4.5.0 (2017-05-22)
157 |
158 | Breaking changes
159 |
160 | - None
161 |
162 | Added
163 |
164 | - Support for `user_id` column to be a `uuid` type
165 | [#333](https://github.com/collectiveidea/audited/pull/333)
166 |
167 | Fixed
168 |
169 | - Fix retrieval of user from controller when populated in before callbacks
170 | [#336](https://github.com/collectiveidea/audited/issues/336)
171 | - Fix column type check in serializer for Oracle DB adapter
172 | [#335](https://github.com/collectiveidea/audited/pull/335)
173 | - Fix `non_audited_columns` to allow symbol names
174 | [#351](https://github.com/collectiveidea/audited/pull/351)
175 |
176 | ## 4.4.1 (2017-03-29)
177 |
178 | Fixed
179 |
180 | - Fix ActiveRecord gem dependency to permit 5.1
181 | [#332](https://github.com/collectiveidea/audited/pull/332)
182 |
183 | ## 4.4.0 (2017-03-29)
184 |
185 | Breaking changes
186 |
187 | - None
188 |
189 | Added
190 |
191 | - Support for `audited_changes` to be a `json` or `jsonb` column in PostgreSQL
192 | [#216](https://github.com/collectiveidea/audited/issues/216)
193 | - Allow `Audited::Audit` to be subclassed by configuring `Audited.audit_class`
194 | [#314](https://github.com/collectiveidea/audited/issues/314)
195 | - Support for Ruby on Rails 5.1
196 | [#329](https://github.com/collectiveidea/audited/issues/329)
197 | - Support for Ruby 2.4
198 | [#329](https://github.com/collectiveidea/audited/issues/329)
199 |
200 | Changed
201 |
202 | - Remove rails-observer dependency
203 | [#325](https://github.com/collectiveidea/audited/issues/325)
204 | - Undeprecated `Audited.audit_class` reader
205 | [#314](https://github.com/collectiveidea/audited/issues/314)
206 |
207 | Fixed
208 |
209 | - SQL error in Rails Conditional GET (304 caching)
210 | [#295](https://github.com/collectiveidea/audited/pull/295)
211 | - Fix missing non_audited_columns= configuration setter
212 | [#320](https://github.com/collectiveidea/audited/issues/320)
213 | - Fix migration generators to specify AR migration version
214 | [#329](https://github.com/collectiveidea/audited/issues/329)
215 |
216 | ## 4.3.0 (2016-09-17)
217 |
218 | Breaking changes
219 |
220 | - None
221 |
222 | Added
223 |
224 | - Support singular arguments for options: `on` and `only`
225 |
226 | Fixed
227 |
228 | - Fix auditing instance attributes if "only" option specified
229 | - Allow private / protected callback declarations
230 | - Do not eagerly connect to database
231 |
232 | ## 4.2.2 (2016-08-01)
233 |
234 | - Correct auditing_enabled for STI models
235 | - Properly set table name for mongomapper
236 |
237 | ## 4.2.1 (2016-07-29)
238 |
239 | - Fix bug when only: is a single field.
240 | - update gemspec to use mongomapper 0.13
241 | - sweeper need not run observer for mongomapper
242 | - Make temporary disabling of auditing threadsafe
243 | - Centralize `Audited.store` as thread safe variable store
244 |
245 | ## 4.2.0 (2015-03-31)
246 |
247 | Not yet documented.
248 |
249 | ## 4.0.0 (2014-09-04)
250 |
251 | Not yet documented.
252 |
253 | ## 4.0.0.rc1 (2014-07-30)
254 |
255 | Not yet documented.
256 |
257 | ## 3.0.0 (2012-09-25)
258 |
259 | Not yet documented.
260 |
261 | ## 3.0.0.rc2 (2012-07-09)
262 |
263 | Not yet documented.
264 |
265 | ## 3.0.0.rc1 (2012-04-25)
266 |
267 | Not yet documented.
268 |
269 | ## 2012-04-10
270 |
271 | - Add Audit scopes for creates, updates and destroys [chriswfx]
272 |
273 | ## 2011-10-25
274 |
275 | - Made ignored_attributes configurable [senny]
276 |
277 | ## 2011-09-09
278 |
279 | - Rails 3.x support
280 | - Support for associated audits
281 | - Support for remote IP address storage
282 | - Plenty of bug fixes and refactoring
283 | - [kennethkalmer, ineu, PatrickMa, jrozner, dwarburton, bsiggelkow, dgm]
284 |
285 | ## 2009-01-27
286 |
287 | - Store old and new values for updates, and store all attributes on destroy.
288 | - Refactored revisioning methods to work as expected
289 |
290 | ## 2008-10-10
291 |
292 | - changed to make it work in development mode
293 |
294 | ## 2008-09-24
295 |
296 | - Add ability to record parent record of the record being audited [Kenneth Kalmer]
297 |
298 | ## 2008-04-19
299 |
300 | - refactored to make compatible with dirty tracking in edge rails
301 | and to stop storing both old and new values in a single audit
302 |
303 | ## 2008-04-18
304 |
305 | - Fix NoMethodError when trying to access the :previous revision
306 | on a model that doesn't have previous revisions [Alex Soto]
307 |
308 | ## 2008-03-21
309 |
310 | - added #changed_attributes to get access to the changes before a
311 | save [Chris Parker]
312 |
313 | ## 2007-12-16
314 |
315 | - Added #revision_at for retrieving a revision from a specific
316 | time [Jacob Atzen]
317 |
318 | ## 2007-12-16
319 |
320 | - Fix error when getting revision from audit with no changes
321 | [Geoffrey Wiseman]
322 |
323 | ## 2007-12-16
324 |
325 | - Remove dependency on acts_as_list
326 |
327 | ## 2007-06-17
328 |
329 | - Added support getting previous revisions
330 |
331 | ## 2006-11-17
332 |
333 | - Replaced use of singleton User.current_user with cache sweeper
334 | implementation for auditing the user that made the change
335 |
336 | ## 2006-11-17
337 |
338 | - added migration generator
339 |
340 | ## 2006-08-14
341 |
342 | - incorporated changes from Michael Schuerig to write_attribute
343 | that saves the new value after every change and not just the
344 | first, and performs proper type-casting before doing comparisons
345 |
346 | ## 2006-08-14
347 |
348 | - The "changes" are now saved as a serialized hash
349 |
350 | ## 2006-07-21
351 |
352 | - initial version
353 |
--------------------------------------------------------------------------------
/spec/audited/audit_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | SingleCov.covered!
4 |
5 | describe Audited::Audit do
6 | let(:user) { Models::ActiveRecord::User.new name: "Testing" }
7 |
8 | describe "audit class" do
9 | around(:example) do |example|
10 | original_audit_class = Audited.audit_class
11 |
12 | class CustomAudit < Audited::Audit
13 | def custom_method
14 | "I'm custom!"
15 | end
16 | end
17 |
18 | class TempModel < ::ActiveRecord::Base
19 | self.table_name = :companies
20 | end
21 |
22 | example.run
23 |
24 | Audited.config { |config| config.audit_class = original_audit_class }
25 | Audited::Audit.audited_class_names.delete("TempModel")
26 | Object.send(:remove_const, :TempModel)
27 | Object.send(:remove_const, :CustomAudit)
28 | end
29 |
30 | context "when a custom audit class is configured" do
31 | it "should be used in place of #{described_class}" do
32 | Audited.config { |config| config.audit_class = CustomAudit }
33 | TempModel.audited
34 |
35 | record = TempModel.create
36 |
37 | audit = record.audits.first
38 | expect(audit).to be_a CustomAudit
39 | expect(audit.custom_method).to eq "I'm custom!"
40 | end
41 | end
42 |
43 | context "when a custom audit class is not configured" do
44 | it "should default to #{described_class}" do
45 | TempModel.audited
46 |
47 | record = TempModel.create
48 |
49 | audit = record.audits.first
50 | expect(audit).to be_a Audited::Audit
51 | expect(audit.respond_to?(:custom_method)).to be false
52 | end
53 | end
54 | end
55 |
56 | describe "#audited_changes" do
57 | let(:audit) { Audited.audit_class.new }
58 |
59 | it "can unserialize yaml from text columns" do
60 | audit.audited_changes = {foo: "bar"}
61 | expect(audit.audited_changes).to eq foo: "bar"
62 | end
63 |
64 | it "does not unserialize from binary columns" do
65 | allow(Audited.audit_class.columns_hash["audited_changes"]).to receive(:type).and_return("foo")
66 | audit.audited_changes = {foo: "bar"}
67 | expect(audit.audited_changes).to eq "{:foo=>\"bar\"}"
68 | end
69 | end
70 |
71 | describe "#undo" do
72 | let(:user) { Models::ActiveRecord::User.create(name: "John") }
73 |
74 | it "undos changes" do
75 | user.update_attribute(:name, 'Joe')
76 | user.audits.last.undo
77 | user.reload
78 | expect(user.name).to eq("John")
79 | end
80 |
81 | it "undos destroy" do
82 | user.destroy
83 | user.audits.last.undo
84 | user = Models::ActiveRecord::User.find_by(name: "John")
85 | expect(user.name).to eq("John")
86 | end
87 |
88 | it "undos creation" do
89 | user # trigger create
90 | expect {user.audits.last.undo}.to change(Models::ActiveRecord::User, :count).by(-1)
91 | end
92 |
93 | it "fails when trying to undo unknown" do
94 | audit = user.audits.last
95 | audit.action = 'oops'
96 | expect { audit.undo }.to raise_error("invalid action given oops")
97 | end
98 | end
99 |
100 | describe "user=" do
101 | it "should be able to set the user to a model object" do
102 | subject.user = user
103 | expect(subject.user).to eq(user)
104 | end
105 |
106 | it "should be able to set the user to nil" do
107 | subject.user_id = 1
108 | subject.user_type = 'Models::ActiveRecord::User'
109 | subject.username = 'joe'
110 |
111 | subject.user = nil
112 |
113 | expect(subject.user).to be_nil
114 | expect(subject.user_id).to be_nil
115 | expect(subject.user_type).to be_nil
116 | expect(subject.username).to be_nil
117 | end
118 |
119 | it "should be able to set the user to a string" do
120 | subject.user = 'test'
121 | expect(subject.user).to eq('test')
122 | end
123 |
124 | it "should clear model when setting to a string" do
125 | subject.user = user
126 | subject.user = 'testing'
127 | expect(subject.user_id).to be_nil
128 | expect(subject.user_type).to be_nil
129 | end
130 |
131 | it "should clear the username when setting to a model" do
132 | subject.username = 'test'
133 | subject.user = user
134 | expect(subject.username).to be_nil
135 | end
136 | end
137 |
138 | describe "revision" do
139 | it "should recreate attributes" do
140 | user = Models::ActiveRecord::User.create name: "1"
141 | 5.times {|i| user.update_attribute :name, (i + 2).to_s }
142 |
143 | user.audits.each do |audit|
144 | expect(audit.revision.name).to eq(audit.version.to_s)
145 | end
146 | end
147 |
148 | it "should set protected attributes" do
149 | u = Models::ActiveRecord::User.create(name: "Brandon")
150 | u.update_attribute :logins, 1
151 | u.update_attribute :logins, 2
152 |
153 | expect(u.audits[2].revision.logins).to eq(2)
154 | expect(u.audits[1].revision.logins).to eq(1)
155 | expect(u.audits[0].revision.logins).to eq(0)
156 | end
157 |
158 | it "should bypass attribute assignment wrappers" do
159 | u = Models::ActiveRecord::User.create(name: "")
160 | expect(u.audits.first.revision.name).to eq("<Joe>")
161 | end
162 |
163 | it "should work for deleted records" do
164 | user = Models::ActiveRecord::User.create name: "1"
165 | user.destroy
166 | revision = user.audits.last.revision
167 | expect(revision.name).to eq(user.name)
168 | expect(revision).to be_a_new_record
169 | end
170 | end
171 |
172 | describe ".collection_cache_key" do
173 | if ActiveRecord::VERSION::MAJOR >= 5
174 | it "uses created at" do
175 | Audited::Audit.delete_all
176 | audit = Models::ActiveRecord::User.create(name: "John").audits.last
177 | audit.update_columns(created_at: Time.parse('2018-01-01'))
178 | expect(Audited::Audit.collection_cache_key).to match(/-20180101\d+$/)
179 | end
180 | else
181 | it "is not defined" do
182 | expect { Audited::Audit.collection_cache_key }.to raise_error(NoMethodError)
183 | end
184 | end
185 | end
186 |
187 | describe ".assign_revision_attributes" do
188 | it "dups when frozen" do
189 | user.freeze
190 | assigned = Audited::Audit.assign_revision_attributes(user, name: "Bar")
191 | expect(assigned.name).to eq "Bar"
192 | end
193 |
194 | it "ignores unassignable attributes" do
195 | assigned = Audited::Audit.assign_revision_attributes(user, oops: "Bar")
196 | expect(assigned.name).to eq "Testing"
197 | end
198 | end
199 |
200 | it "should set the version number on create" do
201 | user = Models::ActiveRecord::User.create! name: "Set Version Number"
202 | expect(user.audits.first.version).to eq(1)
203 | user.update_attribute :name, "Set to 2"
204 | expect(user.audits.reload.first.version).to eq(1)
205 | expect(user.audits.reload.last.version).to eq(2)
206 | user.destroy
207 | expect(Audited::Audit.where(auditable_type: "Models::ActiveRecord::User", auditable_id: user.id).last.version).to eq(3)
208 | end
209 |
210 | it "should set the request uuid on create" do
211 | user = Models::ActiveRecord::User.create! name: "Set Request UUID"
212 | expect(user.audits.reload.first.request_uuid).not_to be_blank
213 | end
214 |
215 | describe "reconstruct_attributes" do
216 | it "should work with the old way of storing just the new value" do
217 | audits = Audited::Audit.reconstruct_attributes([Audited::Audit.new(audited_changes: {"attribute" => "value"})])
218 | expect(audits["attribute"]).to eq("value")
219 | end
220 | end
221 |
222 | describe "audited_classes" do
223 | class Models::ActiveRecord::CustomUser < ::ActiveRecord::Base
224 | end
225 | class Models::ActiveRecord::CustomUserSubclass < Models::ActiveRecord::CustomUser
226 | audited
227 | end
228 |
229 | it "should include audited classes" do
230 | expect(Audited::Audit.audited_classes).to include(Models::ActiveRecord::User)
231 | end
232 |
233 | it "should include subclasses" do
234 | expect(Audited::Audit.audited_classes).to include(Models::ActiveRecord::CustomUserSubclass)
235 | end
236 | end
237 |
238 | describe "new_attributes" do
239 | it "should return a hash of the new values" do
240 | new_attributes = Audited::Audit.new(audited_changes: {a: [1, 2], b: [3, 4]}).new_attributes
241 | expect(new_attributes).to eq({"a" => 2, "b" => 4})
242 | end
243 | end
244 |
245 | describe "old_attributes" do
246 | it "should return a hash of the old values" do
247 | old_attributes = Audited::Audit.new(audited_changes: {a: [1, 2], b: [3, 4]}).old_attributes
248 | expect(old_attributes).to eq({"a" => 1, "b" => 3})
249 | end
250 | end
251 |
252 | describe "as_user" do
253 | it "should record user objects" do
254 | Audited::Audit.as_user(user) do
255 | company = Models::ActiveRecord::Company.create name: "The auditors"
256 | company.name = "The Auditors, Inc"
257 | company.save
258 |
259 | company.audits.each do |audit|
260 | expect(audit.user).to eq(user)
261 | end
262 | end
263 | end
264 |
265 | it "should support nested as_user" do
266 | Audited::Audit.as_user("sidekiq") do
267 | company = Models::ActiveRecord::Company.create name: "The auditors"
268 | company.name = "The Auditors, Inc"
269 | company.save
270 | expect(company.audits[-1].user).to eq("sidekiq")
271 |
272 | Audited::Audit.as_user(user) do
273 | company.name = "NEW Auditors, Inc"
274 | company.save
275 | expect(company.audits[-1].user).to eq(user)
276 | end
277 |
278 | company.name = "LAST Auditors, Inc"
279 | company.save
280 | expect(company.audits[-1].user).to eq("sidekiq")
281 | end
282 | end
283 |
284 | it "should record usernames" do
285 | Audited::Audit.as_user(user.name) do
286 | company = Models::ActiveRecord::Company.create name: "The auditors"
287 | company.name = "The Auditors, Inc"
288 | company.save
289 |
290 | company.audits.each do |audit|
291 | expect(audit.username).to eq(user.name)
292 | end
293 | end
294 | end
295 |
296 | it "should be thread safe" do
297 | begin
298 | expect(user.save).to eq(true)
299 |
300 | t1 = Thread.new do
301 | Audited::Audit.as_user(user) do
302 | sleep 1
303 | expect(Models::ActiveRecord::Company.create(name: "The Auditors, Inc").audits.first.user).to eq(user)
304 | end
305 | end
306 |
307 | t2 = Thread.new do
308 | Audited::Audit.as_user(user.name) do
309 | expect(Models::ActiveRecord::Company.create(name: "The Competing Auditors, LLC").audits.first.username).to eq(user.name)
310 | sleep 0.5
311 | end
312 | end
313 |
314 | t1.join
315 | t2.join
316 | end
317 | end if ActiveRecord::Base.connection.adapter_name != 'SQLite'
318 |
319 | it "should return the value from the yield block" do
320 | result = Audited::Audit.as_user('foo') do
321 | 42
322 | end
323 | expect(result).to eq(42)
324 | end
325 |
326 | it "should reset audited_user when the yield block raises an exception" do
327 | expect {
328 | Audited::Audit.as_user('foo') do
329 | raise StandardError.new('expected')
330 | end
331 | }.to raise_exception('expected')
332 | expect(Audited.store[:audited_user]).to be_nil
333 | end
334 | end
335 | end
336 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Audited [](http://travis-ci.org/collectiveidea/audited) [](https://codeclimate.com/github/collectiveidea/audited) [](https://hakiri.io/github/collectiveidea/audited/master)
2 | =======
3 |
4 | **Audited** (previously acts_as_audited) is an ORM extension that logs all changes to your models. Audited can also record who made those changes, save comments and associate models related to the changes.
5 |
6 | Audited currently (4.x) works with Rails 6.1, Rails 6.0, 5.2, 5.1, 5.0 and 4.2.
7 |
8 | For Rails 3, use gem version 3.0 or see the [3.0-stable branch](https://github.com/collectiveidea/audited/tree/3.0-stable).
9 |
10 | ## Supported Rubies
11 |
12 | Audited supports and is [tested against](http://travis-ci.org/collectiveidea/audited) the following Ruby versions:
13 |
14 | * 2.3.7
15 | * 2.4.4
16 | * 2.5.1
17 | * 2.6.3
18 |
19 | Audited may work just fine with a Ruby version not listed above, but we can't guarantee that it will. If you'd like to maintain a Ruby that isn't listed, please let us know with a [pull request](https://github.com/collectiveidea/audited/pulls).
20 |
21 | ## Supported ORMs
22 |
23 | Audited is currently ActiveRecord-only. In a previous life, Audited worked with MongoMapper. Use the [4.2-stable branch](https://github.com/collectiveidea/audited/tree/4.2-stable) if you need MongoMapper.
24 |
25 | ## Installation
26 |
27 | Add the gem to your Gemfile:
28 |
29 | ```ruby
30 | gem "audited", "~> 4.9"
31 | ```
32 |
33 | Then, from your Rails app directory, create the `audits` table:
34 |
35 | ```bash
36 | $ rails generate audited:install
37 | $ rake db:migrate
38 | ```
39 |
40 | By default changes are stored in YAML format. If you're using PostgreSQL, then you can use `rails generate audited:install --audited-changes-column-type jsonb` (or `json` for MySQL 5.7+ and Rails 5+) to store audit changes natively with database JSON column types.
41 |
42 | If you're using something other than integer primary keys (e.g. UUID) for your User model, then you can use `rails generate audited:install --audited-user-id-column-type uuid` to customize the `audits` table `user_id` column type.
43 |
44 | #### Upgrading
45 |
46 | If you're already using Audited (or acts_as_audited), your `audits` table may require additional columns. After every upgrade, please run:
47 |
48 | ```bash
49 | $ rails generate audited:upgrade
50 | $ rake db:migrate
51 | ```
52 |
53 | Upgrading will only make changes if changes are needed.
54 |
55 |
56 | ## Usage
57 |
58 | Simply call `audited` on your models:
59 |
60 | ```ruby
61 | class User < ActiveRecord::Base
62 | audited
63 | end
64 | ```
65 |
66 | By default, whenever a user is created, updated or destroyed, a new audit is created.
67 |
68 | ```ruby
69 | user = User.create!(name: "Steve")
70 | user.audits.count # => 1
71 | user.update!(name: "Ryan")
72 | user.audits.count # => 2
73 | user.destroy
74 | user.audits.count # => 3
75 | ```
76 |
77 | Audits contain information regarding what action was taken on the model and what changes were made.
78 |
79 | ```ruby
80 | user.update!(name: "Ryan")
81 | audit = user.audits.last
82 | audit.action # => "update"
83 | audit.audited_changes # => {"name"=>["Steve", "Ryan"]}
84 | ```
85 |
86 | You can get previous versions of a record by index or date, or list all
87 | revisions.
88 |
89 | ```ruby
90 | user.revisions
91 | user.revision(1)
92 | user.revision_at(Date.parse("2016-01-01"))
93 | ```
94 |
95 | ### Specifying columns
96 |
97 | By default, a new audit is created for any attribute changes. You can, however, limit the columns to be considered.
98 |
99 | ```ruby
100 | class User < ActiveRecord::Base
101 | # All fields
102 | # audited
103 |
104 | # Single field
105 | # audited only: :name
106 |
107 | # Multiple fields
108 | # audited only: [:name, :address]
109 |
110 | # All except certain fields
111 | # audited except: :password
112 | end
113 | ```
114 |
115 | ### Specifying callbacks
116 |
117 | By default, a new audit is created for any Create, Update or Destroy action. You can, however, limit the actions audited.
118 |
119 | ```ruby
120 | class User < ActiveRecord::Base
121 | # All fields and actions
122 | # audited
123 |
124 | # Single field, only audit Update and Destroy (not Create)
125 | # audited only: :name, on: [:update, :destroy]
126 | end
127 | ```
128 |
129 | ### Comments
130 |
131 | You can attach comments to each audit using an `audit_comment` attribute on your model.
132 |
133 | ```ruby
134 | user.update!(name: "Ryan", audit_comment: "Changing name, just because")
135 | user.audits.last.comment # => "Changing name, just because"
136 | ```
137 |
138 | You can optionally add the `:comment_required` option to your `audited` call to require comments for all audits.
139 |
140 | ```ruby
141 | class User < ActiveRecord::Base
142 | audited :comment_required => true
143 | end
144 | ```
145 |
146 | You can update an audit if only audit_comment is present. You can optionally add the `:update_with_comment_only` option set to `false` to your `audited` call to turn this behavior off for all audits.
147 |
148 | ```ruby
149 | class User < ActiveRecord::Base
150 | audited :update_with_comment_only => false
151 | end
152 | ```
153 |
154 | ### Limiting stored audits
155 |
156 | You can limit the number of audits stored for your model. To configure limiting for all audited models, put the following in an initializer:
157 |
158 | ```ruby
159 | Audited.max_audits = 10 # keep only 10 latest audits
160 | ```
161 |
162 | or customize per model:
163 |
164 | ```ruby
165 | class User < ActiveRecord::Base
166 | audited max_audits: 2
167 | end
168 | ```
169 |
170 | Whenever an object is updated or destroyed, extra audits are combined with newer ones and the old ones are destroyed.
171 |
172 | ```ruby
173 | user = User.create!(name: "Steve")
174 | user.audits.count # => 1
175 | user.update!(name: "Ryan")
176 | user.audits.count # => 2
177 | user.destroy
178 | user.audits.count # => 2
179 | ```
180 |
181 | ### Current User Tracking
182 |
183 | If you're using Audited in a Rails application, all audited changes made within a request will automatically be attributed to the current user. By default, Audited uses the `current_user` method in your controller.
184 |
185 | ```ruby
186 | class PostsController < ApplicationController
187 | def create
188 | current_user # => #
189 | @post = Post.create(params[:post])
190 | @post.audits.last.user # => #
191 | end
192 | end
193 | ```
194 |
195 | To use a method other than `current_user`, put the following in an initializer:
196 |
197 | ```ruby
198 | Audited.current_user_method = :authenticated_user
199 | ```
200 |
201 | Outside of a request, Audited can still record the user with the `as_user` method:
202 |
203 | ```ruby
204 | Audited.audit_class.as_user(User.find(1)) do
205 | post.update!(title: "Hello, world!")
206 | end
207 | post.audits.last.user # => #
208 | ```
209 |
210 | The standard Audited install assumes your User model has an integer primary key type. If this isn't true (e.g. you're using UUID primary keys), you'll need to create a migration to update the `audits` table `user_id` column type. (See Installation above for generator flags if you'd like to regenerate the install migration.)
211 |
212 | #### Custom Audit User
213 |
214 | You might need to use a custom auditor from time to time. This can be done by simply passing in a string:
215 |
216 | ```ruby
217 | class ApplicationController < ActionController::Base
218 | def authenticated_user
219 | if current_user
220 | current_user
221 | else
222 | 'Elon Musk'
223 | end
224 | end
225 | end
226 | ```
227 |
228 | `as_user` also accepts a string, which can be useful for auditing updates made in a CLI environment:
229 |
230 | ```rb
231 | Audited.audit_class.as_user("console-user-#{ENV['SSH_USER']}") do
232 | post.update_attributes!(title: "Hello, world!")
233 | end
234 | post.audits.last.user # => 'console-user-username'
235 | ```
236 |
237 | ### Associated Audits
238 |
239 | Sometimes it's useful to associate an audit with a model other than the one being changed. For instance, given the following models:
240 |
241 | ```ruby
242 | class User < ActiveRecord::Base
243 | belongs_to :company
244 | audited
245 | end
246 |
247 | class Company < ActiveRecord::Base
248 | has_many :users
249 | end
250 | ```
251 |
252 | Every change to a user is audited, but what if you want to grab all of the audits of users belonging to a particular company? You can add the `:associated_with` option to your `audited` call:
253 |
254 | ```ruby
255 | class User < ActiveRecord::Base
256 | belongs_to :company
257 | audited associated_with: :company
258 | end
259 |
260 | class Company < ActiveRecord::Base
261 | has_many :users
262 | has_associated_audits
263 | end
264 | ```
265 |
266 | Now, when an audit is created for a user, that user's company is also saved alongside the audit. This makes it much easier (and faster) to access audits indirectly related to a company.
267 |
268 | ```ruby
269 | company = Company.create!(name: "Collective Idea")
270 | user = company.users.create!(name: "Steve")
271 | user.update!(name: "Steve Richert")
272 | user.audits.last.associated # => #
273 | company.associated_audits.last.auditable # => #
274 | ```
275 |
276 | You can access records' own audits and associated audits in one go:
277 | ```ruby
278 | company.own_and_associated_audits
279 | ```
280 |
281 | ### Conditional auditing
282 |
283 | If you want to audit only under specific conditions, you can provide conditional options (similar to ActiveModel callbacks) that will ensure your model is only audited for these conditions.
284 |
285 | ```ruby
286 | class User < ActiveRecord::Base
287 | audited if: :active?
288 |
289 | private
290 |
291 | def active?
292 | last_login > 6.months.ago
293 | end
294 | end
295 | ```
296 |
297 | Just like in ActiveModel, you can use an inline Proc in your conditions:
298 |
299 | ```ruby
300 | class User < ActiveRecord::Base
301 | audited unless: Proc.new { |u| u.ninja? }
302 | end
303 | ```
304 |
305 | In the above case, the user will only be audited when `User#ninja` is `false`.
306 |
307 | ### Disabling auditing
308 |
309 | If you want to disable auditing temporarily doing certain tasks, there are a few
310 | methods available.
311 |
312 | To disable auditing on a save:
313 |
314 | ```ruby
315 | @user.save_without_auditing
316 | ```
317 |
318 | or:
319 |
320 | ```ruby
321 | @user.without_auditing do
322 | @user.save
323 | end
324 | ```
325 |
326 | To disable auditing on a column:
327 |
328 | ```ruby
329 | User.non_audited_columns = [:first_name, :last_name]
330 | ```
331 |
332 | To disable auditing on an entire model:
333 |
334 | ```ruby
335 | User.auditing_enabled = false
336 | ```
337 |
338 | To disable auditing on all models:
339 |
340 | ```ruby
341 | Audited.auditing_enabled = false
342 | ```
343 |
344 | If you have auditing disabled by default on your model you can enable auditing
345 | temporarily.
346 |
347 | ```ruby
348 | User.auditing_enabled = false
349 | @user.save_with_auditing
350 | ```
351 |
352 | or:
353 |
354 | ```ruby
355 | User.auditing_enabled = false
356 | @user.with_auditing do
357 | @user.save
358 | end
359 | ```
360 |
361 | ### Custom `Audit` model
362 |
363 | If you want to extend or modify the audit model, create a new class that
364 | inherits from `Audited::Audit`:
365 | ```ruby
366 | class CustomAudit < Audited::Audit
367 | def some_custom_behavior
368 | "Hiya!"
369 | end
370 | end
371 | ```
372 | Then set it in an initializer:
373 | ```ruby
374 | # config/initializers/audited.rb
375 |
376 | Audited.config do |config|
377 | config.audit_class = CustomAudit
378 | end
379 | ```
380 |
381 | ## Support
382 |
383 | You can find documentation at: http://rdoc.info/github/collectiveidea/audited
384 |
385 | Or join the [mailing list](http://groups.google.com/group/audited) to get help or offer suggestions.
386 |
387 | ## Contributing
388 |
389 | In the spirit of [free software](http://www.fsf.org/licensing/essays/free-sw.html), **everyone** is encouraged to help improve this project. Here are a few ways _you_ can pitch in:
390 |
391 | * Use prerelease versions of Audited.
392 | * [Report bugs](https://github.com/collectiveidea/audited/issues).
393 | * Fix bugs and submit [pull requests](http://github.com/collectiveidea/audited/pulls).
394 | * Write, clarify or fix documentation.
395 | * Refactor code.
396 |
--------------------------------------------------------------------------------
/lib/audited/auditor.rb:
--------------------------------------------------------------------------------
1 | module Audited
2 | # Specify this act if you want changes to your model to be saved in an
3 | # audit table. This assumes there is an audits table ready.
4 | #
5 | # class User < ActiveRecord::Base
6 | # audited
7 | # end
8 | #
9 | # To store an audit comment set model.audit_comment to your comment before
10 | # a create, update or destroy operation.
11 | #
12 | # See Audited::Auditor::ClassMethods#audited
13 | # for configuration options
14 | module Auditor #:nodoc:
15 | extend ActiveSupport::Concern
16 |
17 | CALLBACKS = [:audit_create, :audit_update, :audit_destroy]
18 |
19 | module ClassMethods
20 | # == Configuration options
21 | #
22 | #
23 | # * +only+ - Only audit the given attributes
24 | # * +except+ - Excludes fields from being saved in the audit log.
25 | # By default, Audited will audit all but these fields:
26 | #
27 | # [self.primary_key, inheritance_column, 'lock_version', 'created_at', 'updated_at']
28 | # You can add to those by passing one or an array of fields to skip.
29 | #
30 | # class User < ActiveRecord::Base
31 | # audited except: :password
32 | # end
33 | #
34 | # * +require_comment+ - Ensures that audit_comment is supplied before
35 | # any create, update or destroy operation.
36 | # * +max_audits+ - Limits the number of stored audits.
37 |
38 | # * +redacted+ - Changes to these fields will be logged, but the values
39 | # will not. This is useful, for example, if you wish to audit when a
40 | # password is changed, without saving the actual password in the log.
41 | # To store values as something other than '[REDACTED]', pass an argument
42 | # to the redaction_value option.
43 | #
44 | # class User < ActiveRecord::Base
45 | # audited redacted: :password, redaction_value: SecureRandom.uuid
46 | # end
47 | #
48 | # * +if+ - Only audit the model when the given function returns true
49 | # * +unless+ - Only audit the model when the given function returns false
50 | #
51 | # class User < ActiveRecord::Base
52 | # audited :if => :active?
53 | #
54 | # def active?
55 | # self.status == 'active'
56 | # end
57 | # end
58 | #
59 | def audited(options = {})
60 | # don't allow multiple calls
61 | return if included_modules.include?(Audited::Auditor::AuditedInstanceMethods)
62 |
63 | extend Audited::Auditor::AuditedClassMethods
64 | include Audited::Auditor::AuditedInstanceMethods
65 |
66 | class_attribute :audit_associated_with, instance_writer: false
67 | class_attribute :audited_options, instance_writer: false
68 | attr_accessor :audit_version, :audit_comment
69 |
70 | self.audited_options = options
71 | normalize_audited_options
72 |
73 | self.audit_associated_with = audited_options[:associated_with]
74 |
75 | if audited_options[:comment_required]
76 | validate :presence_of_audit_comment
77 | before_destroy :require_comment if audited_options[:on].include?(:destroy)
78 | end
79 |
80 | has_many :audits, -> { order(version: :asc) }, as: :auditable, class_name: Audited.audit_class.name, inverse_of: :auditable
81 | Audited.audit_class.audited_class_names << to_s
82 |
83 | after_create :audit_create if audited_options[:on].include?(:create)
84 | before_update :audit_update if audited_options[:on].include?(:update)
85 | before_destroy :audit_destroy if audited_options[:on].include?(:destroy)
86 |
87 | # Define and set after_audit and around_audit callbacks. This might be useful if you want
88 | # to notify a party after the audit has been created or if you want to access the newly-created
89 | # audit.
90 | define_callbacks :audit
91 | set_callback :audit, :after, :after_audit, if: lambda { respond_to?(:after_audit, true) }
92 | set_callback :audit, :around, :around_audit, if: lambda { respond_to?(:around_audit, true) }
93 |
94 | enable_auditing
95 | end
96 |
97 | def has_associated_audits
98 | has_many :associated_audits, as: :associated, class_name: Audited.audit_class.name
99 | end
100 | end
101 |
102 | module AuditedInstanceMethods
103 | REDACTED = '[REDACTED]'
104 |
105 | # Temporarily turns off auditing while saving.
106 | def save_without_auditing
107 | without_auditing { save }
108 | end
109 |
110 | # Executes the block with the auditing callbacks disabled.
111 | #
112 | # @foo.without_auditing do
113 | # @foo.save
114 | # end
115 | #
116 | def without_auditing(&block)
117 | self.class.without_auditing(&block)
118 | end
119 |
120 | # Temporarily turns on auditing while saving.
121 | def save_with_auditing
122 | with_auditing { save }
123 | end
124 |
125 | # Executes the block with the auditing callbacks enabled.
126 | #
127 | # @foo.with_auditing do
128 | # @foo.save
129 | # end
130 | #
131 | def with_auditing(&block)
132 | self.class.with_auditing(&block)
133 | end
134 |
135 | # Gets an array of the revisions available
136 | #
137 | # user.revisions.each do |revision|
138 | # user.name
139 | # user.version
140 | # end
141 | #
142 | def revisions(from_version = 1)
143 | return [] unless audits.from_version(from_version).exists?
144 |
145 | all_audits = audits.select([:audited_changes, :version]).to_a
146 | targeted_audits = all_audits.select { |audit| audit.version >= from_version }
147 |
148 | previous_attributes = reconstruct_attributes(all_audits - targeted_audits)
149 |
150 | targeted_audits.map do |audit|
151 | previous_attributes.merge!(audit.new_attributes)
152 | revision_with(previous_attributes.merge!(version: audit.version))
153 | end
154 | end
155 |
156 | # Get a specific revision specified by the version number, or +:previous+
157 | # Returns nil for versions greater than revisions count
158 | def revision(version)
159 | if version == :previous || self.audits.last.version >= version
160 | revision_with Audited.audit_class.reconstruct_attributes(audits_to(version))
161 | end
162 | end
163 |
164 | # Find the oldest revision recorded prior to the date/time provided.
165 | def revision_at(date_or_time)
166 | audits = self.audits.up_until(date_or_time)
167 | revision_with Audited.audit_class.reconstruct_attributes(audits) unless audits.empty?
168 | end
169 |
170 | # List of attributes that are audited.
171 | def audited_attributes
172 | audited_attributes = attributes.except(*self.class.non_audited_columns)
173 | normalize_enum_changes(audited_attributes)
174 | end
175 |
176 | # Returns a list combined of record audits and associated audits.
177 | def own_and_associated_audits
178 | Audited.audit_class.unscoped
179 | .where('(auditable_type = :type AND auditable_id = :id) OR (associated_type = :type AND associated_id = :id)',
180 | type: self.class.name, id: id)
181 | .order(created_at: :desc)
182 | end
183 |
184 | # Combine multiple audits into one.
185 | def combine_audits(audits_to_combine)
186 | combine_target = audits_to_combine.last
187 | combine_target.audited_changes = audits_to_combine.pluck(:audited_changes).reduce(&:merge)
188 | combine_target.comment = "#{combine_target.comment}\nThis audit is the result of multiple audits being combined."
189 |
190 | transaction do
191 | combine_target.save!
192 | audits_to_combine.unscope(:limit).where("version < ?", combine_target.version).delete_all
193 | end
194 | end
195 |
196 | protected
197 |
198 | def revision_with(attributes)
199 | dup.tap do |revision|
200 | revision.id = id
201 | revision.send :instance_variable_set, '@new_record', destroyed?
202 | revision.send :instance_variable_set, '@persisted', !destroyed?
203 | revision.send :instance_variable_set, '@readonly', false
204 | revision.send :instance_variable_set, '@destroyed', false
205 | revision.send :instance_variable_set, '@_destroyed', false
206 | revision.send :instance_variable_set, '@marked_for_destruction', false
207 | Audited.audit_class.assign_revision_attributes(revision, attributes)
208 |
209 | # Remove any association proxies so that they will be recreated
210 | # and reference the correct object for this revision. The only way
211 | # to determine if an instance variable is a proxy object is to
212 | # see if it responds to certain methods, as it forwards almost
213 | # everything to its target.
214 | revision.instance_variables.each do |ivar|
215 | proxy = revision.instance_variable_get ivar
216 | if !proxy.nil? && proxy.respond_to?(:proxy_respond_to?)
217 | revision.instance_variable_set ivar, nil
218 | end
219 | end
220 | end
221 | end
222 |
223 | private
224 |
225 | def audited_changes
226 | all_changes = respond_to?(:changes_to_save) ? changes_to_save : changes
227 | filtered_changes = \
228 | if audited_options[:only].present?
229 | all_changes.slice(*self.class.audited_columns)
230 | else
231 | all_changes.except(*self.class.non_audited_columns)
232 | end
233 |
234 | filtered_changes = redact_values(filtered_changes)
235 | filtered_changes = normalize_enum_changes(filtered_changes)
236 | filtered_changes.to_hash
237 | end
238 |
239 | def normalize_enum_changes(changes)
240 | self.class.defined_enums.each do |name, values|
241 | if changes.has_key?(name)
242 | changes[name] = \
243 | if changes[name].is_a?(Array)
244 | changes[name].map { |v| values[v] }
245 | elsif rails_below?('5.0')
246 | changes[name]
247 | else
248 | values[changes[name]]
249 | end
250 | end
251 | end
252 | changes
253 | end
254 |
255 | def redact_values(filtered_changes)
256 | [audited_options[:redacted]].flatten.compact.each do |option|
257 | changes = filtered_changes[option.to_s]
258 | new_value = audited_options[:redaction_value] || REDACTED
259 | if changes.is_a? Array
260 | values = changes.map { new_value }
261 | else
262 | values = new_value
263 | end
264 | hash = Hash[option.to_s, values]
265 | filtered_changes.merge!(hash)
266 | end
267 |
268 | filtered_changes
269 | end
270 |
271 | def rails_below?(rails_version)
272 | Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new(rails_version)
273 | end
274 |
275 | def audits_to(version = nil)
276 | if version == :previous
277 | version = if self.audit_version
278 | self.audit_version - 1
279 | else
280 | previous = audits.descending.offset(1).first
281 | previous ? previous.version : 1
282 | end
283 | end
284 | audits.to_version(version)
285 | end
286 |
287 | def audit_create
288 | write_audit(action: 'create', audited_changes: audited_attributes,
289 | comment: audit_comment)
290 | end
291 |
292 | def audit_update
293 | unless (changes = audited_changes).empty? && (audit_comment.blank? || audited_options[:update_with_comment_only] == false)
294 | write_audit(action: 'update', audited_changes: changes,
295 | comment: audit_comment)
296 | end
297 | end
298 |
299 | def audit_destroy
300 | write_audit(action: 'destroy', audited_changes: audited_attributes,
301 | comment: audit_comment) unless new_record?
302 | end
303 |
304 | def write_audit(attrs)
305 | attrs[:associated] = send(audit_associated_with) unless audit_associated_with.nil?
306 | self.audit_comment = nil
307 |
308 | if auditing_enabled
309 | run_callbacks(:audit) {
310 | audit = audits.create(attrs)
311 | combine_audits_if_needed if attrs[:action] != 'create'
312 | audit
313 | }
314 | end
315 | end
316 |
317 | def presence_of_audit_comment
318 | if comment_required_state?
319 | errors.add(:audit_comment, "Comment can't be blank!") unless audit_comment.present?
320 | end
321 | end
322 |
323 | def comment_required_state?
324 | auditing_enabled &&
325 | ((audited_options[:on].include?(:create) && self.new_record?) ||
326 | (audited_options[:on].include?(:update) && self.persisted? && self.changed?))
327 | end
328 |
329 | def combine_audits_if_needed
330 | max_audits = audited_options[:max_audits]
331 | if max_audits && (extra_count = audits.count - max_audits) > 0
332 | audits_to_combine = audits.limit(extra_count + 1)
333 | combine_audits(audits_to_combine)
334 | end
335 | end
336 |
337 | def require_comment
338 | if auditing_enabled && audit_comment.blank?
339 | errors.add(:audit_comment, "Comment can't be blank!")
340 | return false if Rails.version.start_with?('4.')
341 | throw(:abort)
342 | end
343 | end
344 |
345 | CALLBACKS.each do |attr_name|
346 | alias_method "#{attr_name}_callback".to_sym, attr_name
347 | end
348 |
349 | def auditing_enabled
350 | return run_conditional_check(audited_options[:if]) &&
351 | run_conditional_check(audited_options[:unless], matching: false) &&
352 | self.class.auditing_enabled
353 | end
354 |
355 | def run_conditional_check(condition, matching: true)
356 | return true if condition.blank?
357 | return condition.call(self) == matching if condition.respond_to?(:call)
358 | return send(condition) == matching if respond_to?(condition.to_sym, true)
359 |
360 | true
361 | end
362 |
363 | def reconstruct_attributes(audits)
364 | attributes = {}
365 | audits.each { |audit| attributes.merge!(audit.new_attributes) }
366 | attributes
367 | end
368 | end # InstanceMethods
369 |
370 | module AuditedClassMethods
371 | # Returns an array of columns that are audited. See non_audited_columns
372 | def audited_columns
373 | @audited_columns ||= column_names - non_audited_columns
374 | end
375 |
376 | # We have to calculate this here since column_names may not be available when `audited` is called
377 | def non_audited_columns
378 | @non_audited_columns ||= calculate_non_audited_columns
379 | end
380 |
381 | def non_audited_columns=(columns)
382 | @audited_columns = nil # reset cached audited columns on assignment
383 | @non_audited_columns = columns.map(&:to_s)
384 | end
385 |
386 | # Executes the block with auditing disabled.
387 | #
388 | # Foo.without_auditing do
389 | # @foo.save
390 | # end
391 | #
392 | def without_auditing
393 | auditing_was_enabled = auditing_enabled
394 | disable_auditing
395 | yield
396 | ensure
397 | enable_auditing if auditing_was_enabled
398 | end
399 |
400 | # Executes the block with auditing enabled.
401 | #
402 | # Foo.with_auditing do
403 | # @foo.save
404 | # end
405 | #
406 | def with_auditing
407 | auditing_was_enabled = auditing_enabled
408 | enable_auditing
409 | yield
410 | ensure
411 | disable_auditing unless auditing_was_enabled
412 | end
413 |
414 | def disable_auditing
415 | self.auditing_enabled = false
416 | end
417 |
418 | def enable_auditing
419 | self.auditing_enabled = true
420 | end
421 |
422 | # All audit operations during the block are recorded as being
423 | # made by +user+. This is not model specific, the method is a
424 | # convenience wrapper around
425 | # @see Audit#as_user.
426 | def audit_as(user, &block)
427 | Audited.audit_class.as_user(user, &block)
428 | end
429 |
430 | def auditing_enabled
431 | Audited.store.fetch("#{table_name}_auditing_enabled", true) && Audited.auditing_enabled
432 | end
433 |
434 | def auditing_enabled=(val)
435 | Audited.store["#{table_name}_auditing_enabled"] = val
436 | end
437 |
438 | def default_ignored_attributes
439 | [primary_key, inheritance_column] | Audited.ignored_attributes
440 | end
441 |
442 | protected
443 |
444 | def normalize_audited_options
445 | audited_options[:on] = Array.wrap(audited_options[:on])
446 | audited_options[:on] = [:create, :update, :destroy] if audited_options[:on].empty?
447 | audited_options[:only] = Array.wrap(audited_options[:only]).map(&:to_s)
448 | audited_options[:except] = Array.wrap(audited_options[:except]).map(&:to_s)
449 | max_audits = audited_options[:max_audits] || Audited.max_audits
450 | audited_options[:max_audits] = Integer(max_audits).abs if max_audits
451 | end
452 |
453 | def calculate_non_audited_columns
454 | if audited_options[:only].present?
455 | (column_names | default_ignored_attributes) - audited_options[:only]
456 | elsif audited_options[:except].present?
457 | default_ignored_attributes | audited_options[:except]
458 | else
459 | default_ignored_attributes
460 | end
461 | end
462 | end
463 | end
464 | end
465 |
--------------------------------------------------------------------------------
/spec/audited/auditor_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | SingleCov.covered! uncovered: 13 # not testing proxy_respond_to? hack / 2 methods / deprecation of `version`
4 |
5 | describe Audited::Auditor do
6 |
7 | describe "configuration" do
8 | it "should include instance methods" do
9 | expect(Models::ActiveRecord::User.new).to be_a_kind_of( Audited::Auditor::AuditedInstanceMethods)
10 | end
11 |
12 | it "should include class methods" do
13 | expect(Models::ActiveRecord::User).to be_a_kind_of( Audited::Auditor::AuditedClassMethods )
14 | end
15 |
16 | ['created_at', 'updated_at', 'created_on', 'updated_on', 'lock_version', 'id', 'password'].each do |column|
17 | it "should not audit #{column}" do
18 | expect(Models::ActiveRecord::User.non_audited_columns).to include(column)
19 | end
20 | end
21 |
22 | context "should be configurable which conditions are audited" do
23 | subject { ConditionalCompany.new.send(:auditing_enabled) }
24 |
25 | context "when condition method is private" do
26 | subject { ConditionalPrivateCompany.new.send(:auditing_enabled) }
27 |
28 | before do
29 | class ConditionalPrivateCompany < ::ActiveRecord::Base
30 | self.table_name = 'companies'
31 |
32 | audited if: :foo?
33 |
34 | private def foo?
35 | true
36 | end
37 | end
38 | end
39 |
40 | it { is_expected.to be_truthy }
41 | end
42 |
43 | context "when passing a method name" do
44 | before do
45 | class ConditionalCompany < ::ActiveRecord::Base
46 | self.table_name = 'companies'
47 |
48 | audited if: :public?
49 |
50 | def public?; end
51 | end
52 | end
53 |
54 | context "when conditions are true" do
55 | before { allow_any_instance_of(ConditionalCompany).to receive(:public?).and_return(true) }
56 | it { is_expected.to be_truthy }
57 | end
58 |
59 | context "when conditions are false" do
60 | before { allow_any_instance_of(ConditionalCompany).to receive(:public?).and_return(false) }
61 | it { is_expected.to be_falsey }
62 | end
63 | end
64 |
65 | context "when passing a Proc" do
66 | context "when conditions are true" do
67 | before do
68 | class InclusiveCompany < ::ActiveRecord::Base
69 | self.table_name = 'companies'
70 | audited if: Proc.new { true }
71 | end
72 | end
73 |
74 | subject { InclusiveCompany.new.send(:auditing_enabled) }
75 |
76 | it { is_expected.to be_truthy }
77 | end
78 |
79 | context "when conditions are false" do
80 | before do
81 | class ExclusiveCompany < ::ActiveRecord::Base
82 | self.table_name = 'companies'
83 | audited if: Proc.new { false }
84 | end
85 | end
86 | subject { ExclusiveCompany.new.send(:auditing_enabled) }
87 | it { is_expected.to be_falsey }
88 | end
89 | end
90 | end
91 |
92 | context "should be configurable which conditions aren't audited" do
93 | context "when using a method name" do
94 | before do
95 | class ExclusionaryCompany < ::ActiveRecord::Base
96 | self.table_name = 'companies'
97 |
98 | audited unless: :non_profit?
99 |
100 | def non_profit?; end
101 | end
102 | end
103 |
104 | subject { ExclusionaryCompany.new.send(:auditing_enabled) }
105 |
106 | context "when conditions are true" do
107 | before { allow_any_instance_of(ExclusionaryCompany).to receive(:non_profit?).and_return(true) }
108 | it { is_expected.to be_falsey }
109 | end
110 |
111 | context "when conditions are false" do
112 | before { allow_any_instance_of(ExclusionaryCompany).to receive(:non_profit?).and_return(false) }
113 | it { is_expected.to be_truthy }
114 | end
115 | end
116 |
117 | context "when using a proc" do
118 | context "when conditions are true" do
119 | before do
120 | class ExclusionaryCompany < ::ActiveRecord::Base
121 | self.table_name = 'companies'
122 | audited unless: Proc.new { |c| c.exclusive? }
123 |
124 | def exclusive?
125 | true
126 | end
127 | end
128 | end
129 |
130 | subject { ExclusionaryCompany.new.send(:auditing_enabled) }
131 | it { is_expected.to be_falsey }
132 | end
133 |
134 | context "when conditions are false" do
135 | before do
136 | class InclusiveCompany < ::ActiveRecord::Base
137 | self.table_name = 'companies'
138 | audited unless: Proc.new { false }
139 | end
140 | end
141 |
142 | subject { InclusiveCompany.new.send(:auditing_enabled) }
143 | it { is_expected.to be_truthy }
144 | end
145 | end
146 | end
147 |
148 | it "should be configurable which attributes are not audited via ignored_attributes" do
149 | Audited.ignored_attributes = ['delta', 'top_secret', 'created_at']
150 | class Secret < ::ActiveRecord::Base
151 | audited
152 | end
153 |
154 | expect(Secret.non_audited_columns).to include('delta', 'top_secret', 'created_at')
155 | end
156 |
157 | it "should be configurable which attributes are not audited via non_audited_columns=" do
158 | class Secret2 < ::ActiveRecord::Base
159 | audited
160 | self.non_audited_columns = ['delta', 'top_secret', 'created_at']
161 | end
162 |
163 | expect(Secret2.non_audited_columns).to include('delta', 'top_secret', 'created_at')
164 | end
165 |
166 | it "should not save non-audited columns" do
167 | previous = Models::ActiveRecord::User.non_audited_columns
168 | begin
169 | Models::ActiveRecord::User.non_audited_columns += [:favourite_device]
170 |
171 | expect(create_user.audits.first.audited_changes.keys.any? { |col| ['favourite_device', 'created_at', 'updated_at', 'password'].include?( col ) }).to eq(false)
172 | ensure
173 | Models::ActiveRecord::User.non_audited_columns = previous
174 | end
175 | end
176 |
177 | it "should not save other columns than specified in 'only' option" do
178 | user = Models::ActiveRecord::UserOnlyPassword.create
179 | user.instance_eval do
180 | def non_column_attr
181 | @non_column_attr
182 | end
183 |
184 | def non_column_attr=(val)
185 | attribute_will_change!("non_column_attr")
186 | @non_column_attr = val
187 | end
188 | end
189 |
190 | user.password = "password"
191 | user.non_column_attr = "some value"
192 | user.save!
193 | expect(user.audits.last.audited_changes.keys).to eq(%w{password})
194 | end
195 |
196 | it "should save attributes not specified in 'except' option" do
197 | user = Models::ActiveRecord::User.create
198 | user.instance_eval do
199 | def non_column_attr
200 | @non_column_attr
201 | end
202 |
203 | def non_column_attr=(val)
204 | attribute_will_change!("non_column_attr")
205 | @non_column_attr = val
206 | end
207 | end
208 |
209 | user.password = "password"
210 | user.non_column_attr = "some value"
211 | user.save!
212 | expect(user.audits.last.audited_changes.keys).to eq(%w{non_column_attr})
213 | end
214 |
215 | it "should redact columns specified in 'redacted' option" do
216 | redacted = Audited::Auditor::AuditedInstanceMethods::REDACTED
217 | user = Models::ActiveRecord::UserRedactedPassword.create(password: "password")
218 | user.save!
219 | expect(user.audits.last.audited_changes['password']).to eq(redacted)
220 | user.password = "new_password"
221 | user.save!
222 | expect(user.audits.last.audited_changes['password']).to eq([redacted, redacted])
223 | end
224 |
225 | it "should redact columns specified in 'redacted' option when there are multiple specified" do
226 | redacted = Audited::Auditor::AuditedInstanceMethods::REDACTED
227 | user =
228 | Models::ActiveRecord::UserMultipleRedactedAttributes.create(
229 | password: "password",
230 | ssn: 123456789
231 | )
232 | user.save!
233 | expect(user.audits.last.audited_changes['password']).to eq(redacted)
234 | expect(user.audits.last.audited_changes['ssn']).to eq(redacted)
235 | user.password = "new_password"
236 | user.ssn = 987654321
237 | user.save!
238 | expect(user.audits.last.audited_changes['password']).to eq([redacted, redacted])
239 | expect(user.audits.last.audited_changes['ssn']).to eq([redacted, redacted])
240 | end
241 |
242 | it "should redact columns in 'redacted' column with custom option" do
243 | user = Models::ActiveRecord::UserRedactedPasswordCustomRedaction.create(password: "password")
244 | user.save!
245 | expect(user.audits.last.audited_changes['password']).to eq(["My", "Custom", "Value", 7])
246 | end
247 |
248 | if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
249 | describe "'json' and 'jsonb' audited_changes column type" do
250 | let(:migrations_path) { SPEC_ROOT.join("support/active_record/postgres") }
251 |
252 | after do
253 | run_migrations(:down, migrations_path)
254 | end
255 |
256 | it "should work if column type is 'json'" do
257 | run_migrations(:up, migrations_path, 1)
258 | Audited::Audit.reset_column_information
259 | expect(Audited::Audit.columns_hash["audited_changes"].sql_type).to eq("json")
260 |
261 | user = Models::ActiveRecord::User.create
262 | user.name = "new name"
263 | user.save!
264 | expect(user.audits.last.audited_changes).to eq({"name" => [nil, "new name"]})
265 | end
266 |
267 | it "should work if column type is 'jsonb'" do
268 | run_migrations(:up, migrations_path, 2)
269 | Audited::Audit.reset_column_information
270 | expect(Audited::Audit.columns_hash["audited_changes"].sql_type).to eq("jsonb")
271 |
272 | user = Models::ActiveRecord::User.create
273 | user.name = "new name"
274 | user.save!
275 | expect(user.audits.last.audited_changes).to eq({"name" => [nil, "new name"]})
276 | end
277 | end
278 | end
279 | end
280 |
281 | describe :new do
282 | it "should allow mass assignment of all unprotected attributes" do
283 | yesterday = 1.day.ago
284 |
285 | u = Models::ActiveRecord::NoAttributeProtectionUser.new(name: 'name',
286 | username: 'username',
287 | password: 'password',
288 | activated: true,
289 | suspended_at: yesterday,
290 | logins: 2)
291 |
292 | expect(u.name).to eq('name')
293 | expect(u.username).to eq('username')
294 | expect(u.password).to eq('password')
295 | expect(u.activated).to eq(true)
296 | expect(u.suspended_at.to_i).to eq(yesterday.to_i)
297 | expect(u.logins).to eq(2)
298 | end
299 | end
300 |
301 | describe "on create" do
302 | let( :user ) { create_user status: :reliable, audit_comment: "Create" }
303 |
304 | it "should change the audit count" do
305 | expect {
306 | user
307 | }.to change( Audited::Audit, :count ).by(1)
308 | end
309 |
310 | it "should create associated audit" do
311 | expect(user.audits.count).to eq(1)
312 | end
313 |
314 | it "should set the action to create" do
315 | expect(user.audits.first.action).to eq('create')
316 | expect(Audited::Audit.creates.order(:id).last).to eq(user.audits.first)
317 | expect(user.audits.creates.count).to eq(1)
318 | expect(user.audits.updates.count).to eq(0)
319 | expect(user.audits.destroys.count).to eq(0)
320 | end
321 |
322 | it "should store all the audited attributes" do
323 | expect(user.audits.first.audited_changes).to eq(user.audited_attributes)
324 | end
325 |
326 | it "should store enum value" do
327 | expect(user.audits.first.audited_changes["status"]).to eq(1)
328 | end
329 |
330 | it "should store comment" do
331 | expect(user.audits.first.comment).to eq('Create')
332 | end
333 |
334 | it "should not audit an attribute which is excepted if specified on create or destroy" do
335 | on_create_destroy_except_name = Models::ActiveRecord::OnCreateDestroyExceptName.create(name: 'Bart')
336 | expect(on_create_destroy_except_name.audits.first.audited_changes.keys.any?{|col| ['name'].include? col}).to eq(false)
337 | end
338 |
339 | it "should not save an audit if only specified on update/destroy" do
340 | expect {
341 | Models::ActiveRecord::OnUpdateDestroy.create!( name: 'Bart' )
342 | }.to_not change( Audited::Audit, :count )
343 | end
344 | end
345 |
346 | describe "on update" do
347 | before do
348 | @user = create_user( name: 'Brandon', status: :active, audit_comment: 'Update' )
349 | end
350 |
351 | it "should save an audit" do
352 | expect {
353 | @user.update_attribute(:name, "Someone")
354 | }.to change( Audited::Audit, :count ).by(1)
355 | expect {
356 | @user.update_attribute(:name, "Someone else")
357 | }.to change( Audited::Audit, :count ).by(1)
358 | end
359 |
360 | it "should set the action to 'update'" do
361 | @user.update! name: 'Changed'
362 | expect(@user.audits.last.action).to eq('update')
363 | expect(Audited::Audit.updates.order(:id).last).to eq(@user.audits.last)
364 | expect(@user.audits.updates.last).to eq(@user.audits.last)
365 | end
366 |
367 | it "should store the changed attributes" do
368 | @user.update! name: 'Changed'
369 | expect(@user.audits.last.audited_changes).to eq({ 'name' => ['Brandon', 'Changed'] })
370 | end
371 |
372 | it "should store changed enum values" do
373 | @user.update! status: 1
374 | expect(@user.audits.last.audited_changes["status"]).to eq([0, 1])
375 | end
376 |
377 | it "should store audit comment" do
378 | expect(@user.audits.last.comment).to eq('Update')
379 | end
380 |
381 | it "should not save an audit if only specified on create/destroy" do
382 | on_create_destroy = Models::ActiveRecord::OnCreateDestroy.create( name: 'Bart' )
383 | expect {
384 | on_create_destroy.update! name: 'Changed'
385 | }.to_not change( Audited::Audit, :count )
386 | end
387 |
388 | it "should not save an audit if the value doesn't change after type casting" do
389 | @user.update! logins: 0, activated: true
390 | expect { @user.update_attribute :logins, '0' }.to_not change( Audited::Audit, :count )
391 | expect { @user.update_attribute :activated, 1 }.to_not change( Audited::Audit, :count )
392 | expect { @user.update_attribute :activated, '1' }.to_not change( Audited::Audit, :count )
393 | end
394 |
395 | describe "with no dirty changes" do
396 | it "does not create an audit if the record is not changed" do
397 | expect {
398 | @user.save!
399 | }.to_not change( Audited::Audit, :count )
400 | end
401 |
402 | it "creates an audit when an audit comment is present" do
403 | expect {
404 | @user.audit_comment = "Comment"
405 | @user.save!
406 | }.to change( Audited::Audit, :count )
407 | end
408 | end
409 | end
410 |
411 | describe "on destroy" do
412 | before do
413 | @user = create_user(status: :active)
414 | end
415 |
416 | it "should save an audit" do
417 | expect {
418 | @user.destroy
419 | }.to change( Audited::Audit, :count )
420 |
421 | expect(@user.audits.size).to eq(2)
422 | end
423 |
424 | it "should set the action to 'destroy'" do
425 | @user.destroy
426 |
427 | expect(@user.audits.last.action).to eq('destroy')
428 | expect(Audited::Audit.destroys.order(:id).last).to eq(@user.audits.last)
429 | expect(@user.audits.destroys.last).to eq(@user.audits.last)
430 | end
431 |
432 | it "should store all of the audited attributes" do
433 | @user.destroy
434 |
435 | expect(@user.audits.last.audited_changes).to eq(@user.audited_attributes)
436 | end
437 |
438 | it "should store enum value" do
439 | @user.destroy
440 | expect(@user.audits.last.audited_changes["status"]).to eq(0)
441 | end
442 |
443 | it "should be able to reconstruct a destroyed record without history" do
444 | @user.audits.delete_all
445 | @user.destroy
446 |
447 | revision = @user.audits.first.revision
448 | expect(revision.name).to eq(@user.name)
449 | end
450 |
451 | it "should not save an audit if only specified on create/update" do
452 | on_create_update = Models::ActiveRecord::OnCreateUpdate.create!( name: 'Bart' )
453 |
454 | expect {
455 | on_create_update.destroy
456 | }.to_not change( Audited::Audit, :count )
457 | end
458 |
459 | it "should audit dependent destructions" do
460 | owner = Models::ActiveRecord::Owner.create!
461 | company = owner.companies.create!
462 |
463 | expect {
464 | owner.destroy
465 | }.to change( Audited::Audit, :count )
466 |
467 | expect(company.audits.map { |a| a.action }).to eq(['create', 'destroy'])
468 | end
469 | end
470 |
471 | describe "on destroy with unsaved object" do
472 | let(:user) { Models::ActiveRecord::User.new }
473 |
474 | it "should not audit on 'destroy'" do
475 | expect {
476 | user.destroy
477 | }.to_not raise_error
478 |
479 | expect( user.audits ).to be_empty
480 | end
481 | end
482 |
483 | describe "associated with" do
484 | let(:owner) { Models::ActiveRecord::Owner.create(name: 'Models::ActiveRecord::Owner') }
485 | let(:owned_company) { Models::ActiveRecord::OwnedCompany.create!(name: 'The auditors', owner: owner) }
486 |
487 | it "should record the associated object on create" do
488 | expect(owned_company.audits.first.associated).to eq(owner)
489 | end
490 |
491 | it "should store the associated object on update" do
492 | owned_company.update_attribute(:name, 'The Auditors')
493 | expect(owned_company.audits.last.associated).to eq(owner)
494 | end
495 |
496 | it "should store the associated object on destroy" do
497 | owned_company.destroy
498 | expect(owned_company.audits.last.associated).to eq(owner)
499 | end
500 | end
501 |
502 | describe "has associated audits" do
503 | let!(:owner) { Models::ActiveRecord::Owner.create!(name: 'Models::ActiveRecord::Owner') }
504 | let!(:owned_company) { Models::ActiveRecord::OwnedCompany.create!(name: 'The auditors', owner: owner) }
505 |
506 | it "should list the associated audits" do
507 | expect(owner.associated_audits.length).to eq(1)
508 | expect(owner.associated_audits.first.auditable).to eq(owned_company)
509 | end
510 | end
511 |
512 | describe "max_audits" do
513 | it "should respect global setting" do
514 | stub_global_max_audits(10) do
515 | expect(Models::ActiveRecord::User.audited_options[:max_audits]).to eq(10)
516 | end
517 | end
518 |
519 | it "should respect per model setting" do
520 | stub_global_max_audits(10) do
521 | expect(Models::ActiveRecord::MaxAuditsUser.audited_options[:max_audits]).to eq(5)
522 | end
523 | end
524 |
525 | it "should delete old audits when keeped amount exceeded" do
526 | stub_global_max_audits(2) do
527 | user = create_versions(2)
528 | user.update(name: 'John')
529 | expect(user.audits.pluck(:version)).to eq([2, 3])
530 | end
531 | end
532 |
533 | it "should not delete old audits when keeped amount not exceeded" do
534 | stub_global_max_audits(3) do
535 | user = create_versions(2)
536 | user.update(name: 'John')
537 | expect(user.audits.pluck(:version)).to eq([1, 2, 3])
538 | end
539 | end
540 |
541 | it "should delete old extra audits after introducing limit" do
542 | stub_global_max_audits(nil) do
543 | user = Models::ActiveRecord::User.create!(name: 'Brandon', username: 'brandon')
544 | user.update!(name: 'Foobar')
545 | user.update!(name: 'Awesome', username: 'keepers')
546 | user.update!(activated: true)
547 |
548 | Audited.max_audits = 3
549 | Models::ActiveRecord::User.send(:normalize_audited_options)
550 | user.update!(favourite_device: 'Android Phone')
551 | audits = user.audits
552 |
553 | expect(audits.count).to eq(3)
554 | expect(audits[0].audited_changes).to include({'name' => ['Foobar', 'Awesome'], 'username' => ['brandon', 'keepers']})
555 | expect(audits[1].audited_changes).to eq({'activated' => [nil, true]})
556 | expect(audits[2].audited_changes).to eq({'favourite_device' => [nil, 'Android Phone']})
557 | end
558 | end
559 |
560 | it "should add comment line for combined audit" do
561 | stub_global_max_audits(2) do
562 | user = Models::ActiveRecord::User.create!(name: 'Foobar 1')
563 | user.update(name: 'Foobar 2', audit_comment: 'First audit comment')
564 | user.update(name: 'Foobar 3', audit_comment: 'Second audit comment')
565 | expect(user.audits.first.comment).to match(/First audit comment.+is the result of multiple/m)
566 | end
567 | end
568 |
569 | def stub_global_max_audits(max_audits)
570 | previous_max_audits = Audited.max_audits
571 | previous_user_audited_options = Models::ActiveRecord::User.audited_options.dup
572 | begin
573 | Audited.max_audits = max_audits
574 | Models::ActiveRecord::User.send(:normalize_audited_options) # reloads audited_options
575 | yield
576 | ensure
577 | Audited.max_audits = previous_max_audits
578 | Models::ActiveRecord::User.audited_options = previous_user_audited_options
579 | end
580 | end
581 | end
582 |
583 | describe "revisions" do
584 | let( :user ) { create_versions }
585 |
586 | it "should return an Array of Users" do
587 | expect(user.revisions).to be_a_kind_of( Array )
588 | user.revisions.each { |version| expect(version).to be_a_kind_of Models::ActiveRecord::User }
589 | end
590 |
591 | it "should have one revision for a new record" do
592 | expect(create_user.revisions.size).to eq(1)
593 | end
594 |
595 | it "should have one revision for each audit" do
596 | expect(user.audits.size).to eql( user.revisions.size )
597 | end
598 |
599 | it "should set the attributes for each revision" do
600 | u = Models::ActiveRecord::User.create(name: 'Brandon', username: 'brandon')
601 | u.update! name: 'Foobar'
602 | u.update! name: 'Awesome', username: 'keepers'
603 |
604 | expect(u.revisions.size).to eql(3)
605 |
606 | expect(u.revisions[0].name).to eql('Brandon')
607 | expect(u.revisions[0].username).to eql('brandon')
608 |
609 | expect(u.revisions[1].name).to eql('Foobar')
610 | expect(u.revisions[1].username).to eql('brandon')
611 |
612 | expect(u.revisions[2].name).to eql('Awesome')
613 | expect(u.revisions[2].username).to eql('keepers')
614 | end
615 |
616 | it "access to only recent revisions" do
617 | u = Models::ActiveRecord::User.create(name: 'Brandon', username: 'brandon')
618 | u.update! name: 'Foobar'
619 | u.update! name: 'Awesome', username: 'keepers'
620 |
621 | expect(u.revisions(2).size).to eq(2)
622 |
623 | expect(u.revisions(2)[0].name).to eq('Foobar')
624 | expect(u.revisions(2)[0].username).to eq('brandon')
625 |
626 | expect(u.revisions(2)[1].name).to eq('Awesome')
627 | expect(u.revisions(2)[1].username).to eq('keepers')
628 | end
629 |
630 | it "should be empty if no audits exist" do
631 | user.audits.delete_all
632 | expect(user.revisions).to be_empty
633 | end
634 |
635 | it "should ignore attributes that have been deleted" do
636 | user.audits.last.update! audited_changes: {old_attribute: 'old value'}
637 | expect { user.revisions }.to_not raise_error
638 | end
639 | end
640 |
641 | describe "revisions" do
642 | let( :user ) { create_versions(5) }
643 |
644 | it "should maintain identity" do
645 | expect(user.revision(1)).to eq(user)
646 | end
647 |
648 | it "should find the given revision" do
649 | revision = user.revision(3)
650 | expect(revision).to be_a_kind_of( Models::ActiveRecord::User )
651 | expect(revision.audit_version).to eq(3)
652 | expect(revision.name).to eq('Foobar 3')
653 | end
654 |
655 | it "should find the previous revision with :previous" do
656 | revision = user.revision(:previous)
657 | expect(revision.audit_version).to eq(4)
658 | #expect(revision).to eq(user.revision(4))
659 | expect(revision.attributes).to eq(user.revision(4).attributes)
660 | end
661 |
662 | it "should be able to get the previous revision repeatedly" do
663 | previous = user.revision(:previous)
664 | expect(previous.audit_version).to eq(4)
665 | expect(previous.revision(:previous).audit_version).to eq(3)
666 | end
667 |
668 | it "should be able to set protected attributes" do
669 | u = Models::ActiveRecord::User.create(name: 'Brandon')
670 | u.update_attribute :logins, 1
671 | u.update_attribute :logins, 2
672 |
673 | expect(u.revision(3).logins).to eq(2)
674 | expect(u.revision(2).logins).to eq(1)
675 | expect(u.revision(1).logins).to eq(0)
676 | end
677 |
678 | it "should set attributes directly" do
679 | u = Models::ActiveRecord::User.create(name: '')
680 | expect(u.revision(1).name).to eq('<Joe>')
681 | end
682 |
683 | it "should set the attributes for each revision" do
684 | u = Models::ActiveRecord::User.create(name: 'Brandon', username: 'brandon')
685 | u.update! name: 'Foobar'
686 | u.update! name: 'Awesome', username: 'keepers'
687 |
688 | expect(u.revision(3).name).to eq('Awesome')
689 | expect(u.revision(3).username).to eq('keepers')
690 |
691 | expect(u.revision(2).name).to eq('Foobar')
692 | expect(u.revision(2).username).to eq('brandon')
693 |
694 | expect(u.revision(1).name).to eq('Brandon')
695 | expect(u.revision(1).username).to eq('brandon')
696 | end
697 |
698 | it "should correctly restore revision with enum" do
699 | u = Models::ActiveRecord::User.create(status: :active)
700 | u.update_attribute(:status, :reliable)
701 | u.update_attribute(:status, :banned)
702 |
703 | expect(u.revision(3)).to be_banned
704 | expect(u.revision(2)).to be_reliable
705 | expect(u.revision(1)).to be_active
706 | end
707 |
708 | it "should be able to get time for first revision" do
709 | suspended_at = Time.zone.now
710 | u = Models::ActiveRecord::User.create(suspended_at: suspended_at)
711 | expect(u.revision(1).suspended_at.to_s).to eq(suspended_at.to_s)
712 | end
713 |
714 | it "should not raise an error when no previous audits exist" do
715 | user.audits.destroy_all
716 | expect { user.revision(:previous) }.to_not raise_error
717 | end
718 |
719 | it "should mark revision's attributes as changed" do
720 | expect(user.revision(1).name_changed?).to eq(true)
721 | end
722 |
723 | it "should record new audit when saving revision" do
724 | expect {
725 | user.revision(1).save!
726 | }.to change( user.audits, :count ).by(1)
727 | end
728 |
729 | it "should re-insert destroyed records" do
730 | user.destroy
731 | expect {
732 | user.revision(1).save!
733 | }.to change( Models::ActiveRecord::User, :count ).by(1)
734 | end
735 |
736 | it "should return nil for values greater than the number of revisions" do
737 | expect(user.revision(user.revisions.count + 1)).to be_nil
738 | end
739 | end
740 |
741 | describe "revision_at" do
742 | let( :user ) { create_user }
743 |
744 | it "should find the latest revision before the given time" do
745 | audit = user.audits.first
746 | audit.created_at = 1.hour.ago
747 | audit.save!
748 | user.update! name: 'updated'
749 | expect(user.revision_at( 2.minutes.ago ).audit_version).to eq(1)
750 | end
751 |
752 | it "should be nil if given a time before audits" do
753 | expect(user.revision_at( 1.week.ago )).to be_nil
754 | end
755 | end
756 |
757 | describe "own_and_associated_audits" do
758 | it "should return audits for self and associated audits" do
759 | owner = Models::ActiveRecord::Owner.create!
760 | company = owner.companies.create!
761 | company.update!(name: "Collective Idea")
762 |
763 | other_owner = Models::ActiveRecord::Owner.create!
764 | other_owner.companies.create!
765 |
766 | expect(owner.own_and_associated_audits).to match_array(owner.audits + company.audits)
767 | end
768 |
769 | it "should order audits by creation time" do
770 | owner = Models::ActiveRecord::Owner.create!
771 | first_audit = owner.audits.first
772 | first_audit.update_column(:created_at, 1.year.ago)
773 |
774 | company = owner.companies.create!
775 | second_audit = company.audits.first
776 | second_audit.update_column(:created_at, 1.month.ago)
777 |
778 | company.update!(name: "Collective Idea")
779 | third_audit = company.audits.last
780 | expect(owner.own_and_associated_audits.to_a).to eq([third_audit, second_audit, first_audit])
781 | end
782 | end
783 |
784 | describe "without auditing" do
785 | it "should not save an audit when calling #save_without_auditing" do
786 | expect {
787 | u = Models::ActiveRecord::User.new(name: 'Brandon')
788 | expect(u.save_without_auditing).to eq(true)
789 | }.to_not change( Audited::Audit, :count )
790 | end
791 |
792 | it "should not save an audit inside of the #without_auditing block" do
793 | expect {
794 | Models::ActiveRecord::User.without_auditing { Models::ActiveRecord::User.create!( name: 'Brandon' ) }
795 | }.to_not change( Audited::Audit, :count )
796 | end
797 |
798 | it "should reset auditing status even it raises an exception" do
799 | Models::ActiveRecord::User.without_auditing { raise } rescue nil
800 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
801 | end
802 |
803 | it "should be thread safe using a #without_auditing block" do
804 | skip if Models::ActiveRecord::User.connection.class.name.include?("SQLite")
805 |
806 | t1 = Thread.new do
807 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
808 | Models::ActiveRecord::User.without_auditing do
809 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
810 | Models::ActiveRecord::User.create!( name: 'Bart' )
811 | sleep 1
812 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
813 | end
814 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
815 | end
816 |
817 | t2 = Thread.new do
818 | sleep 0.5
819 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
820 | Models::ActiveRecord::User.create!( name: 'Lisa' )
821 | end
822 | t1.join
823 | t2.join
824 |
825 | expect(Models::ActiveRecord::User.find_by_name('Bart').audits.count).to eq(0)
826 | expect(Models::ActiveRecord::User.find_by_name('Lisa').audits.count).to eq(1)
827 | end
828 |
829 | it "should not save an audit when auditing is globally disabled" do
830 | expect(Audited.auditing_enabled).to eq(true)
831 | Audited.auditing_enabled = false
832 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
833 |
834 | user = create_user
835 | expect(user.audits.count).to eq(0)
836 |
837 | Audited.auditing_enabled = true
838 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
839 |
840 | user.update!(name: 'Test')
841 | expect(user.audits.count).to eq(1)
842 | Models::ActiveRecord::User.enable_auditing
843 | end
844 | end
845 |
846 | describe "with auditing" do
847 | it "should save an audit when calling #save_with_auditing" do
848 | expect {
849 | u = Models::ActiveRecord::User.new(name: 'Brandon')
850 | Models::ActiveRecord::User.auditing_enabled = false
851 | expect(u.save_with_auditing).to eq(true)
852 | Models::ActiveRecord::User.auditing_enabled = true
853 | }.to change( Audited::Audit, :count ).by(1)
854 | end
855 |
856 | it "should save an audit inside of the #with_auditing block" do
857 | expect {
858 | Models::ActiveRecord::User.auditing_enabled = false
859 | Models::ActiveRecord::User.with_auditing { Models::ActiveRecord::User.create!( name: 'Brandon' ) }
860 | Models::ActiveRecord::User.auditing_enabled = true
861 | }.to change( Audited::Audit, :count ).by(1)
862 | end
863 |
864 | it "should reset auditing status even it raises an exception" do
865 | Models::ActiveRecord::User.disable_auditing
866 | Models::ActiveRecord::User.with_auditing { raise } rescue nil
867 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
868 | Models::ActiveRecord::User.enable_auditing
869 | end
870 |
871 | it "should be thread safe using a #with_auditing block" do
872 | skip if Models::ActiveRecord::User.connection.class.name.include?("SQLite")
873 |
874 | t1 = Thread.new do
875 | Models::ActiveRecord::User.disable_auditing
876 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
877 | Models::ActiveRecord::User.with_auditing do
878 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
879 |
880 | Models::ActiveRecord::User.create!( name: 'Shaggy' )
881 | sleep 1
882 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
883 | end
884 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
885 | Models::ActiveRecord::User.enable_auditing
886 | end
887 |
888 | t2 = Thread.new do
889 | sleep 0.5
890 | Models::ActiveRecord::User.disable_auditing
891 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
892 | Models::ActiveRecord::User.create!( name: 'Scooby' )
893 | Models::ActiveRecord::User.enable_auditing
894 | end
895 | t1.join
896 | t2.join
897 |
898 | Models::ActiveRecord::User.enable_auditing
899 | expect(Models::ActiveRecord::User.find_by_name('Shaggy').audits.count).to eq(1)
900 | expect(Models::ActiveRecord::User.find_by_name('Scooby').audits.count).to eq(0)
901 | end
902 | end
903 |
904 | describe "comment required" do
905 |
906 | describe "on create" do
907 | it "should not validate when audit_comment is not supplied when initialized" do
908 | expect(Models::ActiveRecord::CommentRequiredUser.new(name: 'Foo')).not_to be_valid
909 | end
910 |
911 | it "should not validate when audit_comment is not supplied trying to create" do
912 | expect(Models::ActiveRecord::CommentRequiredUser.create(name: 'Foo')).not_to be_valid
913 | end
914 |
915 | it "should validate when audit_comment is supplied" do
916 | expect(Models::ActiveRecord::CommentRequiredUser.create(name: 'Foo', audit_comment: 'Create')).to be_valid
917 | end
918 |
919 | it "should validate when audit_comment is not supplied, and creating is not being audited" do
920 | expect(Models::ActiveRecord::OnUpdateCommentRequiredUser.create(name: 'Foo')).to be_valid
921 | expect(Models::ActiveRecord::OnDestroyCommentRequiredUser.create(name: 'Foo')).to be_valid
922 | end
923 |
924 | it "should validate when audit_comment is not supplied, and auditing is disabled" do
925 | Models::ActiveRecord::CommentRequiredUser.disable_auditing
926 | expect(Models::ActiveRecord::CommentRequiredUser.create(name: 'Foo')).to be_valid
927 | Models::ActiveRecord::CommentRequiredUser.enable_auditing
928 | end
929 | end
930 |
931 | describe "on update" do
932 | let( :user ) { Models::ActiveRecord::CommentRequiredUser.create!( audit_comment: 'Create' ) }
933 | let( :on_create_user ) { Models::ActiveRecord::OnDestroyCommentRequiredUser.create }
934 | let( :on_destroy_user ) { Models::ActiveRecord::OnDestroyCommentRequiredUser.create }
935 |
936 | it "should not validate when audit_comment is not supplied" do
937 | expect(user.update(name: 'Test')).to eq(false)
938 | end
939 |
940 | it "should validate when audit_comment is not supplied, and updating is not being audited" do
941 | expect(on_create_user.update(name: 'Test')).to eq(true)
942 | expect(on_destroy_user.update(name: 'Test')).to eq(true)
943 | end
944 |
945 | it "should validate when audit_comment is supplied" do
946 | expect(user.update(name: 'Test', audit_comment: 'Update')).to eq(true)
947 | end
948 |
949 | it "should validate when audit_comment is not supplied, and auditing is disabled" do
950 | Models::ActiveRecord::CommentRequiredUser.disable_auditing
951 | expect(user.update(name: 'Test')).to eq(true)
952 | Models::ActiveRecord::CommentRequiredUser.enable_auditing
953 | end
954 | end
955 |
956 | describe "on destroy" do
957 | let( :user ) { Models::ActiveRecord::CommentRequiredUser.create!( audit_comment: 'Create' )}
958 | let( :on_create_user ) { Models::ActiveRecord::OnCreateCommentRequiredUser.create!( audit_comment: 'Create' ) }
959 | let( :on_update_user ) { Models::ActiveRecord::OnUpdateCommentRequiredUser.create }
960 |
961 | it "should not validate when audit_comment is not supplied" do
962 | expect(user.destroy).to eq(false)
963 | end
964 |
965 | it "should validate when audit_comment is supplied" do
966 | user.audit_comment = "Destroy"
967 | expect(user.destroy).to eq(user)
968 | end
969 |
970 | it "should validate when audit_comment is not supplied, and destroying is not being audited" do
971 | expect(on_create_user.destroy).to eq(on_create_user)
972 | expect(on_update_user.destroy).to eq(on_update_user)
973 | end
974 |
975 | it "should validate when audit_comment is not supplied, and auditing is disabled" do
976 | Models::ActiveRecord::CommentRequiredUser.disable_auditing
977 | expect(user.destroy).to eq(user)
978 | Models::ActiveRecord::CommentRequiredUser.enable_auditing
979 | end
980 | end
981 |
982 | end
983 |
984 | describe "no update with comment only" do
985 | let( :user ) { Models::ActiveRecord::NoUpdateWithCommentOnlyUser.create }
986 |
987 | it "does not create an audit when only an audit_comment is present" do
988 | user.audit_comment = "Comment"
989 | expect { user.save! }.to_not change( Audited::Audit, :count )
990 | end
991 |
992 | end
993 |
994 | describe "attr_protected and attr_accessible" do
995 |
996 | it "should not raise error when attr_accessible is set and protected is false" do
997 | expect {
998 | Models::ActiveRecord::AccessibleAfterDeclarationUser.new(name: 'No fail!')
999 | }.to_not raise_error
1000 | end
1001 |
1002 | it "should not rause an error when attr_accessible is declared before audited" do
1003 | expect {
1004 | Models::ActiveRecord::AccessibleAfterDeclarationUser.new(name: 'No fail!')
1005 | }.to_not raise_error
1006 | end
1007 | end
1008 |
1009 | describe "audit_as" do
1010 | let( :user ) { Models::ActiveRecord::User.create name: 'Testing' }
1011 |
1012 | it "should record user objects" do
1013 | Models::ActiveRecord::Company.audit_as( user ) do
1014 | company = Models::ActiveRecord::Company.create name: 'The auditors'
1015 | company.update! name: 'The Auditors'
1016 |
1017 | company.audits.each do |audit|
1018 | expect(audit.user).to eq(user)
1019 | end
1020 | end
1021 | end
1022 |
1023 | it "should record usernames" do
1024 | Models::ActiveRecord::Company.audit_as( user.name ) do
1025 | company = Models::ActiveRecord::Company.create name: 'The auditors'
1026 | company.update! name: 'The Auditors'
1027 |
1028 | company.audits.each do |audit|
1029 | expect(audit.user).to eq(user.name)
1030 | end
1031 | end
1032 | end
1033 | end
1034 |
1035 | describe "after_audit" do
1036 | let( :user ) { Models::ActiveRecord::UserWithAfterAudit.new }
1037 |
1038 | it "should invoke after_audit callback on create" do
1039 | expect(user.bogus_attr).to be_nil
1040 | expect(user.save).to eq(true)
1041 | expect(user.bogus_attr).to eq("do something")
1042 | end
1043 | end
1044 |
1045 | describe "around_audit" do
1046 | let( :user ) { Models::ActiveRecord::UserWithAfterAudit.new }
1047 |
1048 | it "should invoke around_audit callback on create" do
1049 | expect(user.around_attr).to be_nil
1050 | expect(user.save).to eq(true)
1051 | expect(user.around_attr).to eq(user.audits.last)
1052 | end
1053 | end
1054 |
1055 | describe "STI auditing" do
1056 | it "should correctly disable auditing when using STI" do
1057 | company = Models::ActiveRecord::Company::STICompany.create name: 'The auditors'
1058 | expect(company.type).to eq("Models::ActiveRecord::Company::STICompany")
1059 | expect {
1060 | Models::ActiveRecord::Company.auditing_enabled = false
1061 | company.update! name: 'STI auditors'
1062 | Models::ActiveRecord::Company.auditing_enabled = true
1063 | }.to_not change( Audited::Audit, :count )
1064 | end
1065 | end
1066 | end
1067 |
--------------------------------------------------------------------------------