├── Gemfile
├── .yardopts
├── spec
├── rails_app
│ ├── app
│ │ └── assets
│ │ │ └── config
│ │ │ └── manifest.js
│ └── config
│ │ ├── routes.rb
│ │ ├── initializers
│ │ ├── inflections.rb
│ │ ├── secret_token.rb
│ │ └── backtrace_silencers.rb
│ │ ├── environment.rb
│ │ ├── database.yml
│ │ ├── application.rb
│ │ └── environments
│ │ └── 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
├── audited_spec.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
│ ├── railtie.rb
│ ├── sweeper.rb
│ ├── audit.rb
│ ├── rspec_matchers.rb
│ └── auditor.rb
├── audited-rspec.rb
├── generators
│ └── audited
│ │ ├── migration_helper.rb
│ │ ├── 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.rb
│ │ ├── install_generator.rb
│ │ └── upgrade_generator.rb
└── audited.rb
├── .standard.yml
├── .gitignore
├── gemfiles
├── rails70.gemfile
├── rails71.gemfile
├── rails80.gemfile
├── rails60.gemfile
├── rails61.gemfile
├── rails50.gemfile
├── rails51.gemfile
└── rails52.gemfile
├── .github
└── workflows
│ ├── buildlight.yml
│ ├── publish_gem.yml
│ └── ci.yml
├── 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
├── LICENSE
├── audited.gemspec
├── Appraisals
├── README.md
└── CHANGELOG.md
/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/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link application.js
2 | //= link application.css
3 |
--------------------------------------------------------------------------------
/spec/rails_app/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resources :audits
3 | end
4 |
--------------------------------------------------------------------------------
/lib/audited/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Audited
4 | VERSION = "5.6.0"
5 | end
6 |
--------------------------------------------------------------------------------
/spec/rails_app/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | ActiveSupport::Inflector.inflections do |inflect|
2 | end
3 |
--------------------------------------------------------------------------------
/.standard.yml:
--------------------------------------------------------------------------------
1 | ruby_version: 2.3
2 | ignore:
3 | - lib/generators/audited/templates/**/*
4 | - vendor/bundle/**/*
5 | - gemfiles/vendor/bundle/**/*
6 |
--------------------------------------------------------------------------------
/lib/audited-rspec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "audited/rspec_matchers"
4 | module RSpec::Matchers
5 | include Audited::RspecMatchers
6 | end
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/gemfiles/rails70.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", ">= 7.0.0", "< 7.1"
6 | gem "mysql2", ">= 0.4.4"
7 | gem "pg", ">= 1.1"
8 | gem "sqlite3", ">= 1.4"
9 |
10 | gemspec name: "audited", path: "../"
11 |
--------------------------------------------------------------------------------
/gemfiles/rails71.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", ">= 7.1.0.beta1", "< 7.2"
6 | gem "mysql2", ">= 0.4.4"
7 | gem "pg", ">= 1.1"
8 | gem "sqlite3", ">= 1.4"
9 |
10 | gemspec name: "audited", path: "../"
11 |
--------------------------------------------------------------------------------
/gemfiles/rails80.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", ">= 7.1.0.beta1", "< 8.1"
6 | gem "mysql2", ">= 0.4.4"
7 | gem "pg", ">= 1.1"
8 | gem "sqlite3", ">= 1.4"
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", "< 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/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 |
--------------------------------------------------------------------------------
/.github/workflows/buildlight.yml:
--------------------------------------------------------------------------------
1 | name: Buildlight
2 |
3 | on:
4 | workflow_run:
5 | workflows:
6 | - CI
7 | branches:
8 | - main
9 |
10 | jobs:
11 | webhook:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Webhook
15 | uses: collectiveidea/buildlight@main
16 |
--------------------------------------------------------------------------------
/lib/generators/audited/migration_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Audited
4 | module Generators
5 | module MigrationHelper
6 | def migration_parent
7 | "ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]"
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/add_comment_to_audits.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class <%= migration_class_name %> < <%= migration_parent %>
4 | def self.up
5 | add_column :audits, :comment, :string
6 | end
7 |
8 | def self.down
9 | remove_column :audits, :comment
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/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 | gem "psych", "~> 3.1"
10 | gem "loofah", "2.20.0"
11 |
12 | gemspec name: "audited", path: "../"
13 |
--------------------------------------------------------------------------------
/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 | gem "psych", "~> 3.1"
10 | gem "loofah", "2.20.0"
11 |
12 | gemspec name: "audited", path: "../"
13 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/add_remote_address_to_audits.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class <%= migration_class_name %> < <%= migration_parent %>
4 | def self.up
5 | add_column :audits, :remote_address, :string
6 | end
7 |
8 | def self.down
9 | remove_column :audits, :remote_address
10 | end
11 | end
12 |
13 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/gemfiles/rails52.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", ">= 5.2.8.1", "< 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 | gem "psych", "~> 3.1"
10 | gem "loofah", "2.20.0"
11 |
12 | gemspec name: "audited", path: "../"
13 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/rename_changes_to_audited_changes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class <%= migration_class_name %> < <%= migration_parent %>
4 | def self.up
5 | rename_column :audits, :changes, :audited_changes
6 | end
7 |
8 | def self.down
9 | rename_column :audits, :audited_changes, :changes
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/add_request_uuid_to_audits.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class <%= migration_class_name %> < <%= migration_parent %>
4 | def self.up
5 | add_column :audits, :request_uuid, :string
6 | add_index :audits, :request_uuid
7 | end
8 |
9 | def self.down
10 | remove_column :audits, :request_uuid
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 |
3 | require "bundler/gem_tasks"
4 | require "rspec/core/rake_task"
5 | require "rake/testtask"
6 | require "appraisal"
7 |
8 | RSpec::Core::RakeTask.new(:spec)
9 |
10 | Rake::TestTask.new do |t|
11 | t.libs << "test"
12 | t.test_files = FileList["test/**/*_test.rb"]
13 | t.verbose = true
14 | end
15 |
16 | task default: [:spec, :test]
17 |
--------------------------------------------------------------------------------
/spec/support/active_record/postgres/1_change_audited_changes_type_to_json.rb:
--------------------------------------------------------------------------------
1 | class ChangeAuditedChangesTypeToJson < ActiveRecord::Migration[5.0]
2 | def self.up
3 | remove_column :audits, :audited_changes
4 | add_column :audits, :audited_changes, :json
5 | end
6 |
7 | def self.down
8 | remove_column :audits, :audited_changes
9 | add_column :audits, :audited_changes, :text
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/support/active_record/postgres/2_change_audited_changes_type_to_jsonb.rb:
--------------------------------------------------------------------------------
1 | class ChangeAuditedChangesTypeToJsonb < ActiveRecord::Migration[5.0]
2 | def self.up
3 | remove_column :audits, :audited_changes
4 | add_column :audits, :audited_changes, :jsonb
5 | end
6 |
7 | def self.down
8 | remove_column :audits, :audited_changes
9 | add_column :audits, :audited_changes, :text
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/add_association_to_audits.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class <%= migration_class_name %> < <%= migration_parent %>
4 | def self.up
5 | add_column :audits, :association_id, :integer
6 | add_column :audits, :association_type, :string
7 | end
8 |
9 | def self.down
10 | remove_column :audits, :association_type
11 | remove_column :audits, :association_id
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/rename_parent_to_association.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class <%= migration_class_name %> < <%= migration_parent %>
4 | def self.up
5 | rename_column :audits, :auditable_parent_id, :association_id
6 | rename_column :audits, :auditable_parent_type, :association_type
7 | end
8 |
9 | def self.down
10 | rename_column :audits, :association_type, :auditable_parent_type
11 | rename_column :audits, :association_id, :auditable_parent_id
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/audited/railtie.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Audited
4 | class Railtie < Rails::Railtie
5 | initializer "audited.sweeper" do
6 | ActiveSupport.on_load(:action_controller) do
7 | if defined?(ActionController::Base)
8 | ActionController::Base.around_action Audited::Sweeper.new
9 | end
10 | if defined?(ActionController::API)
11 | ActionController::API.around_action Audited::Sweeper.new
12 | end
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/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: postgres
13 | host: localhost
14 | database: audited_test
15 | min_messages: ERROR
16 |
17 | mysql: &MYSQL
18 | adapter: mysql2
19 | host: localhost
20 | username: root
21 | password: root
22 | database: audited_test
23 | charset: utf8
24 |
25 | test:
26 | <<: *<%= ENV['DB'] || 'SQLITE3MEM' %>
27 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.github/workflows/publish_gem.yml:
--------------------------------------------------------------------------------
1 | name: Publish Gem
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 |
8 | jobs:
9 | push:
10 | if: github.repository == 'collectiveidea/audited'
11 | runs-on: ubuntu-latest
12 | environment: publishing
13 |
14 | permissions:
15 | contents: write
16 | id-token: write
17 |
18 | steps:
19 | # Set up
20 | - uses: actions/checkout@v4
21 | - name: Set up Ruby
22 | uses: ruby/setup-ruby@v1
23 | with:
24 | bundler-cache: true
25 | ruby-version: ruby
26 |
27 | # Release
28 | - uses: rubygems/release-gem@v1
29 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/spec/audited_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Audited do
4 | describe "#store" do
5 | describe "maintains state of store" do
6 | let(:current_user) { Models::ActiveRecord::User.new(name: 'Some User', username: 'some_username') }
7 |
8 | it "can store and retrieve current_user" do
9 | expect(Audited.store[:current_user]).to be_nil
10 |
11 | Audited.store[:current_user] = current_user
12 |
13 | expect(Audited.store[:current_user]).to eq(current_user)
14 | end
15 |
16 | it "checks store is not nil" do
17 | expect(Audited.store).not_to be_nil
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/add_version_to_auditable_index.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class <%= migration_class_name %> < <%= migration_parent %>
4 | def self.up
5 | if index_exists?(:audits, [:auditable_type, :auditable_id], name: index_name)
6 | remove_index :audits, name: index_name
7 | add_index :audits, [:auditable_type, :auditable_id, :version], name: index_name
8 | end
9 | end
10 |
11 | def self.down
12 | if index_exists?(:audits, [:auditable_type, :auditable_id, :version], name: index_name)
13 | remove_index :audits, name: index_name
14 | add_index :audits, [:auditable_type, :auditable_id], name: index_name
15 | end
16 | end
17 |
18 | private
19 |
20 | def index_name
21 | 'auditable_index'
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/revert_polymorphic_indexes_order.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class <%= migration_class_name %> < <%= migration_parent %>
4 | def self.up
5 | fix_index_order_for [:associated_id, :associated_type], 'associated_index'
6 | fix_index_order_for [:auditable_id, :auditable_type], 'auditable_index'
7 | end
8 |
9 | def self.down
10 | fix_index_order_for [:associated_type, :associated_id], 'associated_index'
11 | fix_index_order_for [:auditable_type, :auditable_id], 'auditable_index'
12 | end
13 |
14 | private
15 |
16 | def fix_index_order_for(columns, index_name)
17 | if index_exists? :audits, columns, name: index_name
18 | remove_index :audits, name: index_name
19 | add_index :audits, columns.reverse, name: index_name
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/generators/audited/migration.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Audited
4 | module Generators
5 | module Migration
6 | # Implement the required interface for Rails::Generators::Migration.
7 | def next_migration_number(dirname) # :nodoc:
8 | next_migration_number = current_migration_number(dirname) + 1
9 | if timestamped_migrations?
10 | [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
11 | else
12 | "%.3d" % next_migration_number
13 | end
14 | end
15 |
16 | private
17 |
18 | def timestamped_migrations?
19 | (Rails.gem_version >= Gem::Version.new("7.0")) ?
20 | ::ActiveRecord.timestamped_migrations :
21 | ::ActiveRecord::Base.timestamped_migrations
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/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")].sort.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 |
26 |
--------------------------------------------------------------------------------
/lib/generators/audited/install_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails/generators"
4 | require "rails/generators/migration"
5 | require "active_record"
6 | require "rails/generators/active_record"
7 | require "generators/audited/migration"
8 | require "generators/audited/migration_helper"
9 |
10 | module Audited
11 | module Generators
12 | class InstallGenerator < Rails::Generators::Base
13 | include Rails::Generators::Migration
14 | include Audited::Generators::MigrationHelper
15 | extend Audited::Generators::Migration
16 |
17 | class_option :audited_changes_column_type, type: :string, default: "text", required: false
18 | class_option :audited_user_id_column_type, type: :string, default: "integer", required: false
19 |
20 | source_root File.expand_path("../templates", __FILE__)
21 |
22 | def copy_migration
23 | migration_template "install.rb", "db/migrate/install_audited.rb"
24 | end
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/rename_association_to_associated.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class <%= migration_class_name %> < <%= migration_parent %>
4 | def self.up
5 | if index_exists? :audits, [:association_id, :association_type], :name => 'association_index'
6 | remove_index :audits, :name => 'association_index'
7 | end
8 |
9 | rename_column :audits, :association_id, :associated_id
10 | rename_column :audits, :association_type, :associated_type
11 |
12 | add_index :audits, [:associated_id, :associated_type], :name => 'associated_index'
13 | end
14 |
15 | def self.down
16 | if index_exists? :audits, [:associated_id, :associated_type], :name => 'associated_index'
17 | remove_index :audits, :name => 'associated_index'
18 | end
19 |
20 | rename_column :audits, :associated_type, :association_type
21 | rename_column :audits, :associated_id, :association_id
22 |
23 | add_index :audits, [:association_id, :association_type], :name => 'association_index'
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/audited/sweeper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Audited
4 | class Sweeper
5 | STORED_DATA = {
6 | current_remote_address: :remote_ip,
7 | current_request_uuid: :request_uuid,
8 | current_user: :current_user
9 | }
10 |
11 | delegate :store, to: ::Audited
12 |
13 | def around(controller)
14 | self.controller = controller
15 | STORED_DATA.each { |k, m| store[k] = send(m) }
16 | yield
17 | ensure
18 | self.controller = nil
19 | STORED_DATA.keys.each { |k| store.delete(k) }
20 | end
21 |
22 | def current_user
23 | lambda { controller.send(Audited.current_user_method) if controller.respond_to?(Audited.current_user_method, true) }
24 | end
25 |
26 | def remote_ip
27 | controller.try(:request).try(:remote_ip)
28 | end
29 |
30 | def request_uuid
31 | controller.try(:request).try(:uuid)
32 | end
33 |
34 | def controller
35 | store[:current_controller]
36 | end
37 |
38 | def controller=(value)
39 | store[:current_controller] = value
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/install.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class <%= migration_class_name %> < <%= migration_parent %>
4 | def self.up
5 | create_table :audits, :force => true do |t|
6 | t.column :auditable_id, :integer
7 | t.column :auditable_type, :string
8 | t.column :associated_id, :integer
9 | t.column :associated_type, :string
10 | t.column :user_id, :<%= options[:audited_user_id_column_type] %>
11 | t.column :user_type, :string
12 | t.column :username, :string
13 | t.column :action, :string
14 | t.column :audited_changes, :<%= options[:audited_changes_column_type] %>
15 | t.column :version, :integer, :default => 0
16 | t.column :comment, :string
17 | t.column :remote_address, :string
18 | t.column :request_uuid, :string
19 | t.column :created_at, :datetime
20 | end
21 |
22 | add_index :audits, [:auditable_type, :auditable_id, :version], :name => 'auditable_index'
23 | add_index :audits, [:associated_type, :associated_id], :name => 'associated_index'
24 | add_index :audits, [:user_id, :user_type], :name => 'user_index'
25 | add_index :audits, :request_uuid
26 | add_index :audits, :created_at
27 | end
28 |
29 | def self.down
30 | drop_table :audits
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/spec/audited_spec_helpers.rb:
--------------------------------------------------------------------------------
1 | module AuditedSpecHelpers
2 | def create_user(attrs = {})
3 | Models::ActiveRecord::User.create({name: "Brandon", username: "brandon", password: "password", favourite_device: "Android Phone"}.merge(attrs))
4 | end
5 |
6 | def create_user_with_readonly_attrs(attrs = {})
7 | Models::ActiveRecord::UserWithReadOnlyAttrs.create({name: "Brandon", username: "brandon", password: "password", favourite_device: "Android Phone"}.merge(attrs))
8 | end
9 |
10 | def build_user(attrs = {})
11 | Models::ActiveRecord::User.new({name: "darth", username: "darth", password: "noooooooo"}.merge(attrs))
12 | end
13 |
14 | def create_versions(n = 2, attrs = {})
15 | Models::ActiveRecord::User.create(name: "Foobar 1", **attrs).tap do |u|
16 | (n - 1).times do |i|
17 | u.update_attribute :name, "Foobar #{i + 2}"
18 | end
19 | u.reload
20 | end
21 | end
22 |
23 | def run_migrations(direction, migrations_paths, target_version = nil)
24 | if rails_below?("5.2.0.rc1")
25 | ActiveRecord::Migrator.send(direction, migrations_paths, target_version)
26 | elsif rails_below?("6.0.0.rc1")
27 | ActiveRecord::MigrationContext.new(migrations_paths).send(direction, target_version)
28 | else
29 | ActiveRecord::MigrationContext.new(migrations_paths, ActiveRecord::SchemaMigration).send(direction, target_version)
30 | end
31 | end
32 |
33 | def rails_below?(rails_version)
34 | Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new(rails_version)
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/audited.gemspec:
--------------------------------------------------------------------------------
1 | $:.push File.expand_path("../lib", __FILE__)
2 | require "audited/version"
3 |
4 | Gem::Specification.new do |gem|
5 | gem.name = "audited"
6 | gem.version = Audited::VERSION
7 |
8 | gem.authors = ["Brandon Keepers", "Kenneth Kalmer", "Daniel Morrison", "Brian Ryckbost", "Steve Richert", "Ryan Glover"]
9 | gem.email = "info@collectiveidea.com"
10 | gem.description = "Log all changes to your models"
11 | gem.summary = gem.description
12 | gem.homepage = "https://github.com/collectiveidea/audited"
13 | gem.license = "MIT"
14 |
15 | gem.files = `git ls-files`.split($\).reject { |f| f =~ /^(\.gemspec|\.git|\.standard|\.yard|gemfiles|test|spec)/ }
16 |
17 | gem.required_ruby_version = ">= 2.3.0"
18 |
19 | gem.add_dependency "activerecord", ">= 5.2", "< 8.1"
20 | gem.add_dependency "activesupport", ">= 5.2", "< 8.1"
21 |
22 | gem.add_development_dependency "appraisal"
23 | gem.add_development_dependency "rails", ">= 5.2", "< 8.1"
24 | gem.add_development_dependency "rspec-rails"
25 | gem.add_development_dependency "standard"
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.6"
35 | gem.add_development_dependency "mysql2", ">= 0.3.20"
36 | gem.add_development_dependency "pg", ">= 0.18", "< 2.0"
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/spec/rails_app/config/application.rb:
--------------------------------------------------------------------------------
1 | require "active_record/railtie"
2 |
3 | module RailsApp
4 | class Application < Rails::Application
5 | config.root = File.expand_path("../../", __FILE__)
6 | config.i18n.enforce_available_locales = true
7 |
8 | if Rails.version.start_with?("7.1") && config.active_record.respond_to?(:yaml_column_permitted_classes=)
9 | config.active_record.yaml_column_permitted_classes = [
10 | String,
11 | Symbol,
12 | Integer,
13 | NilClass,
14 | Float,
15 | Time,
16 | Date,
17 | FalseClass,
18 | Hash,
19 | Array,
20 | DateTime,
21 | TrueClass,
22 | BigDecimal,
23 | ActiveSupport::TimeWithZone,
24 | ActiveSupport::TimeZone,
25 | ActiveSupport::HashWithIndifferentAccess
26 | ]
27 | elsif !Rails.version.start_with?("5.0") && !Rails.version.start_with?("5.1") && config.active_record.respond_to?(:yaml_column_permitted_classes=)
28 | config.active_record.yaml_column_permitted_classes =
29 | %w[String Symbol Integer NilClass Float Time Date FalseClass Hash Array DateTime TrueClass BigDecimal
30 | ActiveSupport::TimeWithZone ActiveSupport::TimeZone ActiveSupport::HashWithIndifferentAccess]
31 | end
32 |
33 | if Rails.gem_version >= Gem::Version.new("7.1")
34 | config.active_support.cache_format_version = 7.1
35 | end
36 | end
37 | end
38 |
39 | require "active_record/connection_adapters/sqlite3_adapter"
40 | if ActiveRecord::ConnectionAdapters::SQLite3Adapter.respond_to?(:represent_boolean_as_integer)
41 | ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = true
42 | end
43 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | # Include DB adapters matching the version requirements in
2 | # rails/activerecord/lib/active_record/connection_adapters/*adapter.rb
3 |
4 | appraise "rails50" do
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 | gem "psych", "~> 3.1"
10 | gem "loofah", "2.20.0"
11 | end
12 |
13 | appraise "rails51" do
14 | gem "rails", "~> 5.1.4"
15 | gem "mysql2", ">= 0.3.18", "< 0.6.0"
16 | gem "pg", ">= 0.18", "< 2.0"
17 | gem "sqlite3", "~> 1.3.6"
18 | gem "psych", "~> 3.1"
19 | gem "loofah", "2.20.0"
20 | end
21 |
22 | appraise "rails52" do
23 | gem "rails", ">= 5.2.8.1", "< 5.3"
24 | gem "mysql2", ">= 0.4.4", "< 0.6.0"
25 | gem "pg", ">= 0.18", "< 2.0"
26 | gem "sqlite3", "~> 1.3.6"
27 | gem "psych", "~> 3.1"
28 | gem "loofah", "2.20.0"
29 | end
30 |
31 | appraise "rails60" do
32 | gem "rails", ">= 6.0.0", "< 6.1"
33 | gem "mysql2", ">= 0.4.4"
34 | gem "pg", ">= 0.18", "< 2.0"
35 | gem "sqlite3", "~> 1.4"
36 | end
37 |
38 | appraise "rails61" do
39 | gem "rails", ">= 6.1.0", "< 6.2"
40 | gem "mysql2", ">= 0.4.4"
41 | gem "pg", ">= 1.1", "< 2.0"
42 | gem "sqlite3", "~> 1.4"
43 | end
44 |
45 | appraise "rails70" do
46 | gem "rails", ">= 7.0.0", "< 7.1"
47 | gem "mysql2", ">= 0.4.4"
48 | gem "pg", ">= 1.1"
49 | gem "sqlite3", ">= 1.4"
50 | end
51 |
52 | appraise "rails71" do
53 | gem "rails", ">= 7.1.0.beta1", "< 7.2"
54 | gem "mysql2", ">= 0.4.4"
55 | gem "pg", ">= 1.1"
56 | gem "sqlite3", ">= 1.4"
57 | end
58 |
59 | appraise "rails80" do
60 | gem "rails", ">= 7.1.0.beta1", "< 8.1"
61 | gem "mysql2", ">= 0.4.4"
62 | gem "pg", ">= 1.1"
63 | gem "sqlite3", ">= 1.4"
64 | end
65 |
--------------------------------------------------------------------------------
/lib/audited.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "active_record"
4 |
5 | module Audited
6 | # Wrapper around ActiveSupport::CurrentAttributes
7 | class RequestStore < ActiveSupport::CurrentAttributes
8 | attribute :audited_store
9 | end
10 |
11 | class << self
12 | attr_accessor \
13 | :auditing_enabled,
14 | :current_user_method,
15 | :ignored_attributes,
16 | :ignored_default_callbacks,
17 | :max_audits,
18 | :store_synthesized_enums
19 | attr_writer :audit_class
20 |
21 | def audit_class
22 | # The audit_class is set as String in the initializer. It can not be constantized during initialization and must
23 | # be constantized at runtime. See https://github.com/collectiveidea/audited/issues/608
24 | @audit_class = @audit_class.safe_constantize if @audit_class.is_a?(String)
25 | @audit_class ||= Audited::Audit
26 | end
27 |
28 | # remove audit_model in next major version it was only shortly present in 5.1.0
29 | alias_method :audit_model, :audit_class
30 | deprecate audit_model: "use Audited.audit_class instead of Audited.audit_model. This method will be removed.",
31 | deprecator: ActiveSupport::Deprecation.new('6.0.0', 'Audited')
32 |
33 | def store
34 | RequestStore.audited_store ||= {}
35 | end
36 |
37 | def config
38 | yield(self)
39 | end
40 | end
41 |
42 | @ignored_attributes = %w[lock_version created_at updated_at created_on updated_on]
43 | @ignored_default_callbacks = []
44 |
45 | @current_user_method = :current_user
46 | @auditing_enabled = true
47 | @store_synthesized_enums = false
48 | end
49 |
50 | require "audited/auditor"
51 |
52 | ActiveSupport.on_load :active_record do
53 | require "audited/audit"
54 | include Audited::Auditor
55 | end
56 |
57 | require "audited/sweeper"
58 | require "audited/railtie" if Audited.const_defined?(:Rails)
59 |
--------------------------------------------------------------------------------
/lib/generators/audited/upgrade_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails/generators"
4 | require "rails/generators/migration"
5 | require "active_record"
6 | require "rails/generators/active_record"
7 | require "generators/audited/migration"
8 | require "generators/audited/migration_helper"
9 |
10 | module Audited
11 | module Generators
12 | class UpgradeGenerator < Rails::Generators::Base
13 | include Rails::Generators::Migration
14 | include Audited::Generators::MigrationHelper
15 | extend Audited::Generators::Migration
16 |
17 | source_root File.expand_path("../templates", __FILE__)
18 |
19 | def copy_templates
20 | migrations_to_be_applied do |m|
21 | migration_template "#{m}.rb", "db/migrate/#{m}.rb"
22 | end
23 | end
24 |
25 | private
26 |
27 | def migrations_to_be_applied
28 | Audited::Audit.reset_column_information
29 | columns = Audited::Audit.columns.map(&:name)
30 | indexes = Audited::Audit.connection.indexes(Audited::Audit.table_name)
31 |
32 | yield :add_comment_to_audits unless columns.include?("comment")
33 |
34 | if columns.include?("changes")
35 | yield :rename_changes_to_audited_changes
36 | end
37 |
38 | unless columns.include?("remote_address")
39 | yield :add_remote_address_to_audits
40 | end
41 |
42 | unless columns.include?("request_uuid")
43 | yield :add_request_uuid_to_audits
44 | end
45 |
46 | unless columns.include?("association_id")
47 | if columns.include?("auditable_parent_id")
48 | yield :rename_parent_to_association
49 | else
50 | unless columns.include?("associated_id")
51 | yield :add_association_to_audits
52 | end
53 | end
54 | end
55 |
56 | if columns.include?("association_id")
57 | yield :rename_association_to_associated
58 | end
59 |
60 | if indexes.any? { |i| i.columns == %w[associated_id associated_type] }
61 | yield :revert_polymorphic_indexes_order
62 | end
63 |
64 | if indexes.any? { |i| i.columns == %w[auditable_type auditable_id] }
65 | yield :add_version_to_auditable_index
66 | end
67 | end
68 | end
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/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 |
48 | if ::ActiveRecord::VERSION::MAJOR >= 7
49 | config.active_record.encryption.key_derivation_salt = SecureRandom.hex
50 | config.active_record.encryption.primary_key = SecureRandom.hex
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/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 | assert_includes(content, "class InstallAudited < ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]\n")
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/spec/support/active_record/schema.rb:
--------------------------------------------------------------------------------
1 | require "active_record"
2 | require "logger"
3 |
4 | begin
5 | if ActiveRecord.version >= Gem::Version.new("6.1.0")
6 | db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).first
7 | ActiveRecord::Tasks::DatabaseTasks.create(db_config)
8 | else
9 | db_config = ActiveRecord::Base.configurations[Rails.env].clone
10 | db_type = db_config["adapter"]
11 | db_name = db_config.delete("database")
12 | raise StandardError.new("No database name specified.") if db_name.blank?
13 | if db_type == "sqlite3"
14 | db_file = Pathname.new(__FILE__).dirname.join(db_name)
15 | db_file.unlink if db_file.file?
16 | else
17 | if defined?(JRUBY_VERSION)
18 | db_config.symbolize_keys!
19 | db_config[:configure_connection] = false
20 | end
21 | adapter = ActiveRecord::Base.send("#{db_type}_connection", db_config)
22 | adapter.recreate_database db_name, db_config.slice("charset").symbolize_keys
23 | adapter.disconnect!
24 | end
25 | end
26 | rescue => e
27 | Kernel.warn e
28 | end
29 |
30 | logfile = Pathname.new(__FILE__).dirname.join("debug.log")
31 | logfile.unlink if logfile.file?
32 | ActiveRecord::Base.logger = Logger.new(logfile)
33 |
34 | ActiveRecord::Migration.verbose = false
35 | ActiveRecord::Base.establish_connection
36 |
37 | ActiveRecord::Schema.define do
38 | create_table :users do |t|
39 | t.column :name, :string
40 | t.column :username, :string
41 | t.column :password, :string
42 | t.column :activated, :boolean
43 | t.column :status, :integer, default: 0
44 | t.column :suspended_at, :datetime
45 | t.column :logins, :integer, default: 0
46 | t.column :created_at, :datetime
47 | t.column :updated_at, :datetime
48 | t.column :favourite_device, :string
49 | t.column :ssn, :integer
50 | t.column :phone_numbers, :string
51 | end
52 |
53 | create_table :companies do |t|
54 | t.column :name, :string
55 | t.column :owner_id, :integer
56 | t.column :type, :string
57 | end
58 |
59 | create_table :authors do |t|
60 | t.column :name, :string
61 | end
62 |
63 | create_table :books do |t|
64 | t.column :authord_id, :integer
65 | t.column :title, :string
66 | end
67 |
68 | create_table :audits do |t|
69 | t.column :auditable_id, :integer
70 | t.column :auditable_type, :string
71 | t.column :associated_id, :integer
72 | t.column :associated_type, :string
73 | t.column :user_id, :integer
74 | t.column :user_type, :string
75 | t.column :username, :string
76 | t.column :action, :string
77 | t.column :audited_changes, :text
78 | t.column :version, :integer, default: 0
79 | t.column :comment, :string
80 | t.column :remote_address, :string
81 | t.column :request_uuid, :string
82 | t.column :created_at, :datetime
83 | end
84 |
85 | add_index :audits, [:auditable_id, :auditable_type], name: "auditable_index"
86 | add_index :audits, [:associated_id, :associated_type], name: "associated_index"
87 | add_index :audits, [:user_id, :user_type], name: "user_index"
88 | add_index :audits, :request_uuid
89 | add_index :audits, :created_at
90 | end
91 |
--------------------------------------------------------------------------------
/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 | self.use_transactional_tests = false
10 |
11 | test "should add 'comment' to audits table" do
12 | load_schema 1
13 |
14 | run_generator %w[upgrade]
15 |
16 | assert_migration "db/migrate/add_comment_to_audits.rb" do |content|
17 | assert_match(/add_column :audits, :comment, :string/, content)
18 | end
19 |
20 | assert_migration "db/migrate/rename_changes_to_audited_changes.rb"
21 | end
22 |
23 | test "should rename 'changes' to 'audited_changes'" do
24 | load_schema 2
25 |
26 | run_generator %w[upgrade]
27 |
28 | assert_no_migration "db/migrate/add_comment_to_audits.rb"
29 |
30 | assert_migration "db/migrate/rename_changes_to_audited_changes.rb" do |content|
31 | assert_match(/rename_column :audits, :changes, :audited_changes/, content)
32 | end
33 | end
34 |
35 | test "should add a 'remote_address' to audits table" do
36 | load_schema 3
37 |
38 | run_generator %w[upgrade]
39 |
40 | assert_migration "db/migrate/add_remote_address_to_audits.rb" do |content|
41 | assert_match(/add_column :audits, :remote_address, :string/, content)
42 | end
43 | end
44 |
45 | test "should add 'association_id' and 'association_type' to audits table" do
46 | load_schema 4
47 |
48 | run_generator %w[upgrade]
49 |
50 | assert_migration "db/migrate/add_association_to_audits.rb" do |content|
51 | assert_match(/add_column :audits, :association_id, :integer/, content)
52 | assert_match(/add_column :audits, :association_type, :string/, content)
53 | end
54 | end
55 |
56 | test "should rename 'association_id' to 'associated_id' and 'association_type' to 'associated_type'" do
57 | load_schema 5
58 |
59 | run_generator %w[upgrade]
60 |
61 | assert_migration "db/migrate/rename_association_to_associated.rb" do |content|
62 | assert_match(/rename_column :audits, :association_id, :associated_id/, content)
63 | assert_match(/rename_column :audits, :association_type, :associated_type/, content)
64 | end
65 | end
66 |
67 | test "should add 'request_uuid' to audits table" do
68 | load_schema 6
69 |
70 | run_generator %w[upgrade]
71 |
72 | assert_migration "db/migrate/add_request_uuid_to_audits.rb" do |content|
73 | assert_match(/add_column :audits, :request_uuid, :string/, content)
74 | assert_match(/add_index :audits, :request_uuid/, content)
75 | end
76 | end
77 |
78 | test "should add 'version' to auditable_index" do
79 | load_schema 6
80 |
81 | run_generator %w[upgrade]
82 |
83 | assert_migration "db/migrate/add_version_to_auditable_index.rb" do |content|
84 | assert_match(/add_index :audits, \[:auditable_type, :auditable_id, :version\]/, content)
85 | end
86 | end
87 |
88 | test "generate migration with correct AR migration parent" do
89 | load_schema 1
90 |
91 | run_generator %w[upgrade]
92 |
93 | assert_migration "db/migrate/add_comment_to_audits.rb" do |content|
94 | assert_includes(content, "class AddCommentToAudits < ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]\n")
95 | end
96 | end
97 | end
98 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | - pull_request
5 | - push
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | strategy:
11 | fail-fast: false
12 | matrix:
13 | ruby: [2.3, 2.4, 2.5, 2.6, 2.7, 3.0, 3.1, 3.2]
14 | appraisal:
15 | - rails52
16 | - rails60
17 | - rails61
18 | - rails70
19 | - rails71
20 | - rails80
21 | db: [POSTGRES, MYSQL, SQLITE]
22 | exclude:
23 | # MySQL has issues on Ruby 2.3
24 | # https://github.com/ruby/setup-ruby/issues/150
25 | - ruby: 2.3
26 | db: MYSQL
27 |
28 | # PostgreSQL is segfaulting on 2.3
29 | # Doesn't seem worth solving.
30 | - ruby: 2.3
31 | db: POSTGRES
32 |
33 | # Rails 5.2 supports Ruby 2.2-2.5
34 | - appraisal: rails52
35 | ruby: 2.6
36 | - appraisal: rails52
37 | ruby: 2.7
38 | - appraisal: rails52
39 | ruby: 3.0
40 | - appraisal: rails52
41 | ruby: 3.1
42 | - appraisal: rails52
43 | ruby: 3.2
44 |
45 | # Rails 6.0 supports Ruby 2.5-2.7
46 | - appraisal: rails60
47 | ruby: 2.3
48 | - appraisal: rails60
49 | ruby: 2.4
50 | - appraisal: rails60
51 | ruby: 3.0
52 | - appraisal: rails60
53 | ruby: 3.1
54 | - appraisal: rails60
55 | ruby: 3.2
56 |
57 | # Rails 6.1 supports Ruby 2.5+
58 | - appraisal: rails61
59 | ruby: 2.3
60 | - appraisal: rails61
61 | ruby: 2.4
62 |
63 | # Rails 7 supports Ruby 2.7+
64 | - appraisal: rails70
65 | ruby: 2.3
66 | - appraisal: rails70
67 | ruby: 2.4
68 | - appraisal: rails70
69 | ruby: 2.5
70 | - appraisal: rails70
71 | ruby: 2.6
72 |
73 | # Rails 7.1 supports Ruby 2.7+
74 | - appraisal: rails71
75 | ruby: 2.3
76 | - appraisal: rails71
77 | ruby: 2.4
78 | - appraisal: rails71
79 | ruby: 2.5
80 | - appraisal: rails71
81 | ruby: 2.6
82 | - appraisal: rails80
83 | ruby: 2.6
84 |
85 | services:
86 | postgres:
87 | image: postgres
88 | env:
89 | POSTGRES_USER: postgres
90 | POSTGRES_PASSWORD: postgres
91 | POSTGRES_DB: audited_test
92 | ports:
93 | - 5432:5432
94 | # needed because the postgres container does not provide a healthcheck
95 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
96 |
97 | env:
98 | DB_DATABASE: audited_test
99 | DB_USER: root
100 | DB_PASSWORD: 'root'
101 | DB_HOST: localhost
102 |
103 | steps:
104 | - name: Setup MySQL
105 | run: |
106 | sudo /etc/init.d/mysql start
107 | mysql -e 'CREATE DATABASE audited_test;' -uroot -proot
108 | mysql -e 'SHOW DATABASES;' -uroot -proot
109 | - uses: actions/checkout@v4
110 | - name: Copy Gemfile
111 | run: sed 's/\.\././' gemfiles/${{ matrix.appraisal }}.gemfile > Gemfile
112 | - name: Set up Ruby ${{ matrix.ruby }}
113 | uses: ruby/setup-ruby@v1
114 | with:
115 | ruby-version: ${{ matrix.ruby }}
116 | bundler-cache: true
117 | - name: Run tests
118 | env:
119 | DB: ${{ matrix.db }}
120 | run: bundle exec rake
121 |
--------------------------------------------------------------------------------
/spec/audited/sweeper_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | SingleCov.covered!
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
26 | end
27 | end
28 |
29 | describe AuditsController do
30 | include RSpec::Rails::ControllerExampleGroup
31 | render_views
32 |
33 | before do
34 | Audited::Railtie.initializers.each(&:run)
35 | Audited.current_user_method = :current_user
36 | end
37 |
38 | let(:user) { create_user }
39 |
40 | describe "POST audit" do
41 | it "should audit user" do
42 | controller.send(:current_user=, user)
43 | expect {
44 | post :create
45 | }.to change(Audited::Audit, :count)
46 |
47 | expect(controller.company.audits.last.user).to eq(user)
48 | end
49 |
50 | it "does not audit when method is not found" do
51 | controller.send(:current_user=, user)
52 | Audited.current_user_method = :nope
53 | expect {
54 | post :create
55 | }.to change(Audited::Audit, :count)
56 | expect(controller.company.audits.last.user).to eq(nil)
57 | end
58 |
59 | it "should support custom users for sweepers" do
60 | controller.send(:custom_user=, user)
61 | Audited.current_user_method = :custom_user
62 |
63 | expect {
64 | post :create
65 | }.to change(Audited::Audit, :count)
66 |
67 | expect(controller.company.audits.last.user).to eq(user)
68 | end
69 |
70 | it "should record the remote address responsible for the change" do
71 | request.env["REMOTE_ADDR"] = "1.2.3.4"
72 | controller.send(:current_user=, user)
73 |
74 | post :create
75 |
76 | expect(controller.company.audits.last.remote_address).to eq("1.2.3.4")
77 | end
78 |
79 | it "should record a UUID for the web request responsible for the change" do
80 | allow_any_instance_of(ActionDispatch::Request).to receive(:uuid).and_return("abc123")
81 | controller.send(:current_user=, user)
82 |
83 | post :create
84 |
85 | expect(controller.company.audits.last.request_uuid).to eq("abc123")
86 | end
87 |
88 | it "should call current_user after controller callbacks" do
89 | expect(controller).to receive(:populate_user) do
90 | controller.send(:current_user=, user)
91 | end
92 |
93 | expect {
94 | post :create
95 | }.to change(Audited::Audit, :count)
96 |
97 | expect(controller.company.audits.last.user).to eq(user)
98 | end
99 | end
100 |
101 | describe "PUT update" do
102 | it "should not save blank audits" do
103 | controller.send(:current_user=, user)
104 |
105 | expect {
106 | put :update, params: {id: 123}
107 | }.to_not change(Audited::Audit, :count)
108 | end
109 | end
110 | end
111 |
112 | describe Audited::Sweeper do
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
129 | t2.join
130 |
131 | expect(instance.controller).to be_nil
132 | end
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.gem_version >= Gem::Version.new("5.1")
9 | attr_protected :logins if respond_to?(:attr_protected)
10 | enum status: {active: 0, reliable: 1, banned: 2}
11 |
12 | if Rails.gem_version >= Gem::Version.new("7.1")
13 | serialize :phone_numbers, type: Array
14 | else
15 | serialize :phone_numbers, Array
16 | end
17 |
18 | def name=(val)
19 | write_attribute(:name, CGI.escapeHTML(val))
20 | end
21 | end
22 |
23 | class UserExceptPassword < ::ActiveRecord::Base
24 | self.table_name = :users
25 | audited except: :password
26 | end
27 |
28 | class UserOnlyPassword < ::ActiveRecord::Base
29 | self.table_name = :users
30 | attribute :non_column_attr if Rails.gem_version >= Gem::Version.new("5.1")
31 | audited only: :password
32 | end
33 |
34 | class UserRedactedPassword < ::ActiveRecord::Base
35 | self.table_name = :users
36 | audited redacted: :password
37 | end
38 |
39 | class UserMultipleRedactedAttributes < ::ActiveRecord::Base
40 | self.table_name = :users
41 | audited redacted: [:password, :ssn]
42 | end
43 |
44 | class UserRedactedPasswordCustomRedaction < ::ActiveRecord::Base
45 | self.table_name = :users
46 | audited redacted: :password, redaction_value: ["My", "Custom", "Value", 7]
47 | end
48 |
49 | if ::ActiveRecord::VERSION::MAJOR >= 7
50 | class UserWithEncryptedPassword < ::ActiveRecord::Base
51 | self.table_name = :users
52 | audited
53 | encrypts :password
54 | end
55 | end
56 |
57 | class UserWithReadOnlyAttrs < ::ActiveRecord::Base
58 | self.table_name = :users
59 | audited
60 | attr_readonly :status
61 | end
62 |
63 | class CommentRequiredUser < ::ActiveRecord::Base
64 | self.table_name = :users
65 | audited except: :password, comment_required: true
66 | end
67 |
68 | class OnCreateCommentRequiredUser < ::ActiveRecord::Base
69 | self.table_name = :users
70 | audited comment_required: true, on: :create
71 | end
72 |
73 | class OnUpdateCommentRequiredUser < ::ActiveRecord::Base
74 | self.table_name = :users
75 | audited comment_required: true, on: :update
76 | end
77 |
78 | class OnDestroyCommentRequiredUser < ::ActiveRecord::Base
79 | self.table_name = :users
80 | audited comment_required: true, on: :destroy
81 | end
82 |
83 | class NoUpdateWithCommentOnlyUser < ::ActiveRecord::Base
84 | self.table_name = :users
85 | audited update_with_comment_only: false
86 | end
87 |
88 | class AccessibleAfterDeclarationUser < ::ActiveRecord::Base
89 | self.table_name = :users
90 | audited
91 | attr_accessible :name, :username, :password if respond_to?(:attr_accessible)
92 | end
93 |
94 | class AccessibleBeforeDeclarationUser < ::ActiveRecord::Base
95 | self.table_name = :users
96 | attr_accessible :name, :username, :password if respond_to?(:attr_accessible) # declare attr_accessible before calling aaa
97 | audited
98 | end
99 |
100 | class NoAttributeProtectionUser < ::ActiveRecord::Base
101 | self.table_name = :users
102 | audited
103 | end
104 |
105 | class UserWithAfterAudit < ::ActiveRecord::Base
106 | self.table_name = :users
107 | audited
108 | attr_accessor :bogus_attr, :around_attr
109 |
110 | private
111 |
112 | def after_audit
113 | self.bogus_attr = "do something"
114 | end
115 |
116 | def around_audit
117 | self.around_attr = yield
118 | end
119 | end
120 |
121 | class MaxAuditsUser < ::ActiveRecord::Base
122 | self.table_name = :users
123 | audited max_audits: 5
124 | end
125 |
126 | class Company < ::ActiveRecord::Base
127 | audited
128 | end
129 |
130 | class Company::STICompany < Company
131 | end
132 |
133 | class Owner < ::ActiveRecord::Base
134 | self.table_name = "users"
135 | audited
136 | has_associated_audits
137 | has_many :companies, class_name: "OwnedCompany", dependent: :destroy
138 | accepts_nested_attributes_for :companies
139 | enum status: {active: 0, reliable: 1, banned: 2}
140 | end
141 |
142 | class OwnedCompany < ::ActiveRecord::Base
143 | self.table_name = "companies"
144 | belongs_to :owner, class_name: "Owner", touch: true
145 | attr_accessible :name, :owner if respond_to?(:attr_accessible) # declare attr_accessible before calling aaa
146 | audited associated_with: :owner
147 | end
148 |
149 | class OwnedCompany::STICompany < OwnedCompany
150 | end
151 |
152 | class OnUpdateDestroy < ::ActiveRecord::Base
153 | self.table_name = "companies"
154 | audited on: [:update, :destroy]
155 | end
156 |
157 | class OnCreateDestroy < ::ActiveRecord::Base
158 | self.table_name = "companies"
159 | audited on: [:create, :destroy]
160 | end
161 |
162 | class OnCreateDestroyUser < ::ActiveRecord::Base
163 | self.table_name = "users"
164 | audited on: [:create, :destroy]
165 | end
166 |
167 | class OnCreateDestroyExceptName < ::ActiveRecord::Base
168 | self.table_name = "companies"
169 | audited except: :name, on: [:create, :destroy]
170 | end
171 |
172 | class OnCreateUpdate < ::ActiveRecord::Base
173 | self.table_name = "companies"
174 | audited on: [:create, :update]
175 | end
176 |
177 | class OnTouchOnly < ::ActiveRecord::Base
178 | self.table_name = "users"
179 | audited on: [:touch]
180 | end
181 | end
182 | end
183 |
--------------------------------------------------------------------------------
/lib/audited/audit.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "set"
4 |
5 | module Audited
6 | # Audit saves the changes to ActiveRecord models. It has the following attributes:
7 | #
8 | # * auditable: the ActiveRecord model that was changed
9 | # * user: the user that performed the change; a string or an ActiveRecord model
10 | # * action: one of create, update, or delete
11 | # * audited_changes: a hash of all the changes
12 | # * comment: a comment set with the audit
13 | # * version: the version of the model
14 | # * request_uuid: a uuid based that allows audits from the same controller request
15 | # * created_at: Time that the change was performed
16 | #
17 |
18 | class YAMLIfTextColumnType
19 | class << self
20 | def load(obj)
21 | if text_column?
22 | ActiveRecord::Coders::YAMLColumn.new(Object).load(obj)
23 | else
24 | obj
25 | end
26 | end
27 |
28 | def dump(obj)
29 | if text_column?
30 | ActiveRecord::Coders::YAMLColumn.new(Object).dump(obj)
31 | else
32 | obj
33 | end
34 | end
35 |
36 | def text_column?
37 | Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
38 | end
39 | end
40 | end
41 |
42 | class Audit < ::ActiveRecord::Base
43 | belongs_to :auditable, polymorphic: true
44 | belongs_to :user, polymorphic: true
45 | belongs_to :associated, polymorphic: true
46 |
47 | before_create :set_version_number, :set_audit_user, :set_request_uuid, :set_remote_address
48 |
49 | cattr_accessor :audited_class_names
50 | self.audited_class_names = Set.new
51 |
52 | if Rails.gem_version >= Gem::Version.new("7.1")
53 | serialize :audited_changes, coder: YAMLIfTextColumnType
54 | else
55 | serialize :audited_changes, YAMLIfTextColumnType
56 | end
57 |
58 | scope :ascending, -> { reorder(version: :asc) }
59 | scope :descending, -> { reorder(version: :desc) }
60 | scope :creates, -> { where(action: "create") }
61 | scope :updates, -> { where(action: "update") }
62 | scope :destroys, -> { where(action: "destroy") }
63 |
64 | scope :up_until, ->(date_or_time) { where("created_at <= ?", date_or_time) }
65 | scope :from_version, ->(version) { where("version >= ?", version) }
66 | scope :to_version, ->(version) { where("version <= ?", version) }
67 | scope :auditable_finder, ->(auditable_id, auditable_type) { where(auditable_id: auditable_id, auditable_type: auditable_type) }
68 | # Return all audits older than the current one.
69 | def ancestors
70 | self.class.ascending.auditable_finder(auditable_id, auditable_type).to_version(version)
71 | end
72 |
73 | # Return an instance of what the object looked like at this revision. If
74 | # the object has been destroyed, this will be a new record.
75 | def revision
76 | clazz = auditable_type.constantize
77 | (clazz.find_by_id(auditable_id) || clazz.new).tap do |m|
78 | self.class.assign_revision_attributes(m, self.class.reconstruct_attributes(ancestors).merge(audit_version: version))
79 | end
80 | end
81 |
82 | # Returns a hash of the changed attributes with the new values
83 | def new_attributes
84 | (audited_changes || {}).each_with_object({}.with_indifferent_access) do |(attr, values), attrs|
85 | attrs[attr] = (action == "update") ? values.last : values
86 | end
87 | end
88 |
89 | # Returns a hash of the changed attributes with the old values
90 | def old_attributes
91 | (audited_changes || {}).each_with_object({}.with_indifferent_access) do |(attr, values), attrs|
92 | attrs[attr] = (action == "update") ? values.first : values
93 | end
94 | end
95 |
96 | # Allows user to undo changes
97 | def undo
98 | case action
99 | when "create"
100 | # destroys a newly created record
101 | auditable.destroy!
102 | when "destroy"
103 | # creates a new record with the destroyed record attributes
104 | auditable_type.constantize.create!(audited_changes)
105 | when "update"
106 | # changes back attributes
107 | auditable.update!(audited_changes.transform_values(&:first))
108 | else
109 | raise StandardError, "invalid action given #{action}"
110 | end
111 | end
112 |
113 | # Allows user to be set to either a string or an ActiveRecord object
114 | # @private
115 | def user_as_string=(user)
116 | # reset both either way
117 | self.user_as_model = self.username = nil
118 | user.is_a?(::ActiveRecord::Base) ?
119 | self.user_as_model = user :
120 | self.username = user
121 | end
122 | alias_method :user_as_model=, :user=
123 | alias_method :user=, :user_as_string=
124 |
125 | # @private
126 | def user_as_string
127 | user_as_model || username
128 | end
129 | alias_method :user_as_model, :user
130 | alias_method :user, :user_as_string
131 |
132 | # Returns the list of classes that are being audited
133 | def self.audited_classes
134 | audited_class_names.map(&:constantize)
135 | end
136 |
137 | # All audits made during the block called will be recorded as made
138 | # by +user+. This method is hopefully threadsafe, making it ideal
139 | # for background operations that require audit information.
140 | def self.as_user(user)
141 | last_audited_user = ::Audited.store[:audited_user]
142 | ::Audited.store[:audited_user] = user
143 | yield
144 | ensure
145 | ::Audited.store[:audited_user] = last_audited_user
146 | end
147 |
148 | # @private
149 | def self.reconstruct_attributes(audits)
150 | audits.each_with_object({}) do |audit, all|
151 | all.merge!(audit.new_attributes)
152 | all[:audit_version] = audit.version
153 | end
154 | end
155 |
156 | # @private
157 | def self.assign_revision_attributes(record, attributes)
158 | attributes.each do |attr, val|
159 | record = record.dup if record.frozen?
160 |
161 | if record.respond_to?("#{attr}=")
162 | record.attributes.key?(attr.to_s) ?
163 | record[attr] = val :
164 | record.send("#{attr}=", val)
165 | end
166 | end
167 | record
168 | end
169 |
170 | # use created_at as timestamp cache key
171 | def self.collection_cache_key(collection = all, *)
172 | super(collection, :created_at)
173 | end
174 |
175 | private
176 |
177 | def set_version_number
178 | if action == "create"
179 | self.version = 1
180 | else
181 | collection = (ActiveRecord::VERSION::MAJOR >= 6) ? self.class.unscoped : self.class
182 | max = collection.auditable_finder(auditable_id, auditable_type).maximum(:version) || 0
183 | self.version = max + 1
184 | end
185 | end
186 |
187 | def set_audit_user
188 | self.user ||= ::Audited.store[:audited_user] # from .as_user
189 | self.user ||= ::Audited.store[:current_user].try!(:call) # from Sweeper
190 | nil # prevent stopping callback chains
191 | end
192 |
193 | def set_request_uuid
194 | self.request_uuid ||= ::Audited.store[:current_request_uuid]
195 | self.request_uuid ||= SecureRandom.uuid
196 | end
197 |
198 | def set_remote_address
199 | self.remote_address ||= ::Audited.store[:current_remote_address]
200 | end
201 | end
202 | end
203 |
--------------------------------------------------------------------------------
/lib/audited/rspec_matchers.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Audited
4 | module RspecMatchers
5 | # Ensure that the model is audited.
6 | #
7 | # Options:
8 | # * associated_with - tests that the audit makes use of the associated_with option
9 | # * only - tests that the audit makes use of the only option *Overrides except option*
10 | # * except - tests that the audit makes use of the except option
11 | # * requires_comment - if specified, then the audit must require comments through the audit_comment attribute
12 | # * on - tests that the audit makes use of the on option with specified parameters
13 | #
14 | # Example:
15 | # it { should be_audited }
16 | # it { should be_audited.associated_with(:user) }
17 | # it { should be_audited.only(:field_name) }
18 | # it { should be_audited.except(:password) }
19 | # it { should be_audited.requires_comment }
20 | # it { should be_audited.on(:create).associated_with(:user).except(:password) }
21 | #
22 | def be_audited
23 | AuditMatcher.new
24 | end
25 |
26 | # Ensure that the model has associated audits
27 | #
28 | # Example:
29 | # it { should have_associated_audits }
30 | #
31 | def have_associated_audits
32 | AssociatedAuditMatcher.new
33 | end
34 |
35 | class AuditMatcher # :nodoc:
36 | def initialize
37 | @options = {}
38 | end
39 |
40 | def associated_with(model)
41 | @options[:associated_with] = model
42 | self
43 | end
44 |
45 | def only(*fields)
46 | @options[:only] = fields.flatten.map(&:to_s)
47 | self
48 | end
49 |
50 | def except(*fields)
51 | @options[:except] = fields.flatten.map(&:to_s)
52 | self
53 | end
54 |
55 | def requires_comment
56 | @options[:comment_required] = true
57 | self
58 | end
59 |
60 | def on(*actions)
61 | @options[:on] = actions.flatten.map(&:to_sym)
62 | self
63 | end
64 |
65 | def matches?(subject)
66 | @subject = subject
67 | auditing_enabled? && required_checks_for_options_satisfied?
68 | end
69 |
70 | def failure_message
71 | "Expected #{@expectation}"
72 | end
73 |
74 | def negative_failure_message
75 | "Did not expect #{@expectation}"
76 | end
77 |
78 | alias_method :failure_message_when_negated, :negative_failure_message
79 |
80 | def description
81 | description = "audited"
82 | description += " associated with #{@options[:associated_with]}" if @options.key?(:associated_with)
83 | description += " only => #{@options[:only].join ", "}" if @options.key?(:only)
84 | description += " except => #{@options[:except].join(", ")}" if @options.key?(:except)
85 | description += " requires audit_comment" if @options.key?(:comment_required)
86 |
87 | description
88 | end
89 |
90 | protected
91 |
92 | def expects(message)
93 | @expectation = message
94 | end
95 |
96 | def auditing_enabled?
97 | expects "#{model_class} to be audited"
98 | model_class.respond_to?(:auditing_enabled) && model_class.auditing_enabled
99 | end
100 |
101 | def model_class
102 | @subject.class
103 | end
104 |
105 | def associated_with_model?
106 | expects "#{model_class} to record audits to associated model #{@options[:associated_with]}"
107 | model_class.audit_associated_with == @options[:associated_with]
108 | end
109 |
110 | def records_changes_to_specified_fields?
111 | ignored_fields = build_ignored_fields_from_options
112 |
113 | expects "non audited columns (#{model_class.non_audited_columns.inspect}) to match (#{ignored_fields})"
114 | model_class.non_audited_columns.to_set == ignored_fields.to_set
115 | end
116 |
117 | def comment_required_valid?
118 | expects "to require audit_comment before #{model_class.audited_options[:on]} when comment required"
119 | validate_callbacks_include_presence_of_comment? && destroy_callbacks_include_comment_required?
120 | end
121 |
122 | def only_audit_on_designated_callbacks?
123 | {
124 | create: [:after, :audit_create],
125 | update: [:before, :audit_update],
126 | destroy: [:before, :audit_destroy]
127 | }.map do |(action, kind_callback)|
128 | kind, callback = kind_callback
129 | callbacks_for(action, kind: kind).include?(callback) if @options[:on].include?(action)
130 | end.compact.all?
131 | end
132 |
133 | def validate_callbacks_include_presence_of_comment?
134 | if @options[:comment_required] && audited_on_create_or_update?
135 | callbacks_for(:validate).include?(:presence_of_audit_comment)
136 | else
137 | true
138 | end
139 | end
140 |
141 | def audited_on_create_or_update?
142 | model_class.audited_options[:on].include?(:create) || model_class.audited_options[:on].include?(:update)
143 | end
144 |
145 | def destroy_callbacks_include_comment_required?
146 | if @options[:comment_required] && model_class.audited_options[:on].include?(:destroy)
147 | callbacks_for(:destroy).include?(:require_comment)
148 | else
149 | true
150 | end
151 | end
152 |
153 | def requires_comment_before_callbacks?
154 | [:create, :update, :destroy].map do |action|
155 | if @options[:comment_required] && model_class.audited_options[:on].include?(action)
156 | callbacks_for(action).include?(:require_comment)
157 | end
158 | end.compact.all?
159 | end
160 |
161 | def callbacks_for(action, kind: :before)
162 | model_class.send("_#{action}_callbacks").select { |cb| cb.kind == kind }.map(&:filter)
163 | end
164 |
165 | def build_ignored_fields_from_options
166 | default_ignored_attributes = model_class.default_ignored_attributes
167 |
168 | if @options[:only].present?
169 | (default_ignored_attributes | model_class.column_names) - @options[:only]
170 | elsif @options[:except].present?
171 | default_ignored_attributes | @options[:except]
172 | else
173 | default_ignored_attributes
174 | end
175 | end
176 |
177 | def required_checks_for_options_satisfied?
178 | {
179 | only: :records_changes_to_specified_fields?,
180 | except: :records_changes_to_specified_fields?,
181 | comment_required: :comment_required_valid?,
182 | associated_with: :associated_with_model?,
183 | on: :only_audit_on_designated_callbacks?
184 | }.map do |(option, check)|
185 | send(check) if @options[option].present?
186 | end.compact.all?
187 | end
188 | end
189 |
190 | class AssociatedAuditMatcher # :nodoc:
191 | def matches?(subject)
192 | @subject = subject
193 |
194 | association_exists?
195 | end
196 |
197 | def failure_message
198 | "Expected #{model_class} to have associated audits"
199 | end
200 |
201 | def negative_failure_message
202 | "Expected #{model_class} to not have associated audits"
203 | end
204 |
205 | alias_method :failure_message_when_negated, :negative_failure_message
206 |
207 | def description
208 | "has associated audits"
209 | end
210 |
211 | protected
212 |
213 | def model_class
214 | @subject.class
215 | end
216 |
217 | def reflection
218 | model_class.reflect_on_association(:associated_audits)
219 | end
220 |
221 | def association_exists?
222 | !reflection.nil? &&
223 | reflection.macro == :has_many &&
224 | reflection.options[:class_name] == Audited.audit_class.name
225 | end
226 | end
227 | end
228 | end
229 |
--------------------------------------------------------------------------------
/spec/audited/audit_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | SingleCov.covered! uncovered: 2 # Rails version check
4 |
5 | class CustomAudit < Audited::Audit
6 | def custom_method
7 | "I'm custom!"
8 | end
9 | end
10 |
11 | class TempModel1 < ::ActiveRecord::Base
12 | self.table_name = :companies
13 | end
14 |
15 | class TempModel2 < ::ActiveRecord::Base
16 | self.table_name = :companies
17 | end
18 |
19 | class Models::ActiveRecord::CustomUser < ::ActiveRecord::Base
20 | end
21 |
22 | class Models::ActiveRecord::CustomUserSubclass < Models::ActiveRecord::CustomUser
23 | audited
24 | end
25 |
26 | describe Audited::Audit do
27 | let(:user) { Models::ActiveRecord::User.new name: "Testing" }
28 |
29 | describe "audit class" do
30 | around(:example) do |example|
31 | original_audit_class = Audited.audit_class
32 |
33 | example.run
34 |
35 | Audited.config { |config| config.audit_class = original_audit_class }
36 | end
37 |
38 | context "when a custom audit class is configured" do
39 | it "should be used in place of #{described_class}" do
40 | Audited.config { |config| config.audit_class = "CustomAudit" }
41 | TempModel1.audited
42 |
43 | record = TempModel1.create
44 |
45 | audit = record.audits.first
46 | expect(audit).to be_a CustomAudit
47 | expect(audit.custom_method).to eq "I'm custom!"
48 | end
49 | end
50 |
51 | context "when a custom audit class is not configured" do
52 | it "should default to #{described_class}" do
53 | TempModel2.audited
54 |
55 | record = TempModel2.create
56 |
57 | audit = record.audits.first
58 | expect(audit).to be_a Audited::Audit
59 | expect(audit.respond_to?(:custom_method)).to be false
60 | end
61 | end
62 | end
63 |
64 | describe "#audited_changes" do
65 | let(:audit) { Audited.audit_class.new }
66 |
67 | it "can unserialize yaml from text columns" do
68 | audit.audited_changes = {foo: "bar"}
69 | expect(audit.audited_changes).to eq foo: "bar"
70 | end
71 |
72 | it "does not unserialize from binary columns" do
73 | allow(Audited::YAMLIfTextColumnType).to receive(:text_column?).and_return(false)
74 | audit.audited_changes = {foo: "bar"}
75 | expect(audit.audited_changes).to eq "{:foo=>\"bar\"}"
76 | end
77 | end
78 |
79 | describe "#undo" do
80 | let(:user) { Models::ActiveRecord::User.create(name: "John") }
81 |
82 | it "undos changes" do
83 | user.update_attribute(:name, "Joe")
84 | user.audits.last.undo
85 | user.reload
86 | expect(user.name).to eq("John")
87 | end
88 |
89 | it "undos destroy" do
90 | user.destroy
91 | user.audits.last.undo
92 | user = Models::ActiveRecord::User.find_by(name: "John")
93 | expect(user.name).to eq("John")
94 | end
95 |
96 | it "undos creation" do
97 | user # trigger create
98 | expect { user.audits.last.undo }.to change(Models::ActiveRecord::User, :count).by(-1)
99 | end
100 |
101 | it "fails when trying to undo unknown" do
102 | audit = user.audits.last
103 | audit.action = "oops"
104 | expect { audit.undo }.to raise_error("invalid action given oops")
105 | end
106 | end
107 |
108 | describe "user=" do
109 | it "should be able to set the user to a model object" do
110 | subject.user = user
111 | expect(subject.user).to eq(user)
112 | end
113 |
114 | it "should be able to set the user to nil" do
115 | subject.user_id = 1
116 | subject.user_type = "Models::ActiveRecord::User"
117 | subject.username = "joe"
118 |
119 | subject.user = nil
120 |
121 | expect(subject.user).to be_nil
122 | expect(subject.user_id).to be_nil
123 | expect(subject.user_type).to be_nil
124 | expect(subject.username).to be_nil
125 | end
126 |
127 | it "should be able to set the user to a string" do
128 | subject.user = "test"
129 | expect(subject.user).to eq("test")
130 | end
131 |
132 | it "should clear model when setting to a string" do
133 | subject.user = user
134 | subject.user = "testing"
135 | expect(subject.user_id).to be_nil
136 | expect(subject.user_type).to be_nil
137 | end
138 |
139 | it "should clear the username when setting to a model" do
140 | subject.username = "test"
141 | subject.user = user
142 | expect(subject.username).to be_nil
143 | end
144 | end
145 |
146 | describe "revision" do
147 | it "should recreate attributes" do
148 | user = Models::ActiveRecord::User.create name: "1"
149 | 5.times { |i| user.update_attribute :name, (i + 2).to_s }
150 |
151 | user.audits.each do |audit|
152 | expect(audit.revision.name).to eq(audit.version.to_s)
153 | end
154 | end
155 |
156 | it "should set protected attributes" do
157 | u = Models::ActiveRecord::User.create(name: "Brandon")
158 | u.update_attribute :logins, 1
159 | u.update_attribute :logins, 2
160 |
161 | expect(u.audits[2].revision.logins).to eq(2)
162 | expect(u.audits[1].revision.logins).to eq(1)
163 | expect(u.audits[0].revision.logins).to eq(0)
164 | end
165 |
166 | it "should bypass attribute assignment wrappers" do
167 | u = Models::ActiveRecord::User.create(name: "")
168 | expect(u.audits.first.revision.name).to eq("<Joe>")
169 | end
170 |
171 | it "should work for deleted records" do
172 | user = Models::ActiveRecord::User.create name: "1"
173 | user.destroy
174 | revision = user.audits.last.revision
175 | expect(revision.name).to eq(user.name)
176 | expect(revision).to be_a_new_record
177 | end
178 | end
179 |
180 | describe ".collection_cache_key" do
181 | if ActiveRecord::VERSION::MAJOR >= 5
182 | it "uses created at" do
183 | Audited::Audit.delete_all
184 | audit = Models::ActiveRecord::User.create(name: "John").audits.last
185 | audit.update_columns(created_at: Time.zone.parse("2018-01-01"))
186 | expect(Audited::Audit.collection_cache_key).to match(/-20180101\d+$/)
187 | end
188 | else
189 | it "is not defined" do
190 | expect { Audited::Audit.collection_cache_key }.to raise_error(NoMethodError)
191 | end
192 | end
193 | end
194 |
195 | describe ".assign_revision_attributes" do
196 | it "dups when frozen" do
197 | user.freeze
198 | assigned = Audited::Audit.assign_revision_attributes(user, name: "Bar")
199 | expect(assigned.name).to eq "Bar"
200 | end
201 |
202 | it "ignores unassignable attributes" do
203 | assigned = Audited::Audit.assign_revision_attributes(user, oops: "Bar")
204 | expect(assigned.name).to eq "Testing"
205 | end
206 | end
207 |
208 | it "should set the version number on create" do
209 | user = Models::ActiveRecord::User.create! name: "Set Version Number"
210 | expect(user.audits.first.version).to eq(1)
211 | user.update_attribute :name, "Set to 2"
212 | expect(user.audits.reload.first.version).to eq(1)
213 | expect(user.audits.reload.last.version).to eq(2)
214 | user.destroy
215 | expect(Audited::Audit.where(auditable_type: "Models::ActiveRecord::User", auditable_id: user.id).last.version).to eq(3)
216 | end
217 |
218 | it "should set the request uuid on create" do
219 | user = Models::ActiveRecord::User.create! name: "Set Request UUID"
220 | expect(user.audits.reload.first.request_uuid).not_to be_blank
221 | end
222 |
223 | describe "reconstruct_attributes" do
224 | it "should work with the old way of storing just the new value" do
225 | audits = Audited::Audit.reconstruct_attributes([Audited::Audit.new(audited_changes: {"attribute" => "value"})])
226 | expect(audits["attribute"]).to eq("value")
227 | end
228 | end
229 |
230 | describe "audited_classes" do
231 | it "should include audited classes" do
232 | expect(Audited::Audit.audited_classes).to include(Models::ActiveRecord::User)
233 | end
234 |
235 | it "should include subclasses" do
236 | expect(Audited::Audit.audited_classes).to include(Models::ActiveRecord::CustomUserSubclass)
237 | end
238 | end
239 |
240 | describe "new_attributes" do
241 | it "should return the audited_changes without modification for create" do
242 | new_attributes = Audited::Audit.new(audited_changes: {int: 1, array: [1]}, action: :create).new_attributes
243 | expect(new_attributes).to eq({"int" => 1, "array" => [1]})
244 | end
245 |
246 | it "should return a hash that contains the after values of each attribute" do
247 | new_attributes = Audited::Audit.new(audited_changes: {a: [1, 2], b: [3, 4]}, action: :update).new_attributes
248 | expect(new_attributes).to eq({"a" => 2, "b" => 4})
249 | end
250 |
251 | it "should return the audited_changes without modification for destroy" do
252 | new_attributes = Audited::Audit.new(audited_changes: {int: 1, array: [1]}, action: :destroy).new_attributes
253 | expect(new_attributes).to eq({"int" => 1, "array" => [1]})
254 | end
255 | end
256 |
257 | describe "old_attributes" do
258 | it "should return the audited_changes without modification for create" do
259 | old_attributes = Audited::Audit.new(audited_changes: {int: 1, array: [1]}, action: :create).new_attributes
260 | expect(old_attributes).to eq({"int" => 1, "array" => [1]})
261 | end
262 |
263 | it "should return a hash that contains the before values of each attribute" do
264 | old_attributes = Audited::Audit.new(audited_changes: {a: [1, 2], b: [3, 4]}, action: :update).old_attributes
265 | expect(old_attributes).to eq({"a" => 1, "b" => 3})
266 | end
267 |
268 | it "should return the audited_changes without modification for destroy" do
269 | old_attributes = Audited::Audit.new(audited_changes: {int: 1, array: [1]}, action: :destroy).old_attributes
270 | expect(old_attributes).to eq({"int" => 1, "array" => [1]})
271 | end
272 | end
273 |
274 | describe "as_user" do
275 | it "should record user objects" do
276 | Audited::Audit.as_user(user) do
277 | company = Models::ActiveRecord::Company.create name: "The auditors"
278 | company.name = "The Auditors, Inc"
279 | company.save
280 |
281 | company.audits.each do |audit|
282 | expect(audit.user).to eq(user)
283 | end
284 | end
285 | end
286 |
287 | it "should support nested as_user" do
288 | Audited::Audit.as_user("sidekiq") do
289 | company = Models::ActiveRecord::Company.create name: "The auditors"
290 | company.name = "The Auditors, Inc"
291 | company.save
292 | expect(company.audits[-1].user).to eq("sidekiq")
293 |
294 | Audited::Audit.as_user(user) do
295 | company.name = "NEW Auditors, Inc"
296 | company.save
297 | expect(company.audits[-1].user).to eq(user)
298 | end
299 |
300 | company.name = "LAST Auditors, Inc"
301 | company.save
302 | expect(company.audits[-1].user).to eq("sidekiq")
303 | end
304 | end
305 |
306 | it "should record usernames" do
307 | Audited::Audit.as_user(user.name) do
308 | company = Models::ActiveRecord::Company.create name: "The auditors"
309 | company.name = "The Auditors, Inc"
310 | company.save
311 |
312 | company.audits.each do |audit|
313 | expect(audit.username).to eq(user.name)
314 | end
315 | end
316 | end
317 |
318 | if ActiveRecord::Base.connection.adapter_name != "SQLite"
319 | it "should be thread safe" do
320 | expect(user.save).to eq(true)
321 |
322 | t1 = Thread.new do
323 | Audited::Audit.as_user(user) do
324 | sleep 1
325 | expect(Models::ActiveRecord::Company.create(name: "The Auditors, Inc").audits.first.user).to eq(user)
326 | end
327 | end
328 |
329 | t2 = Thread.new do
330 | Audited::Audit.as_user(user.name) do
331 | expect(Models::ActiveRecord::Company.create(name: "The Competing Auditors, LLC").audits.first.username).to eq(user.name)
332 | sleep 0.5
333 | end
334 | end
335 |
336 | t1.join
337 | t2.join
338 | end
339 | end
340 |
341 | it "should return the value from the yield block" do
342 | result = Audited::Audit.as_user("foo") do
343 | 42
344 | end
345 | expect(result).to eq(42)
346 | end
347 |
348 | it "should reset audited_user when the yield block raises an exception" do
349 | expect {
350 | Audited::Audit.as_user("foo") do
351 | raise StandardError.new("expected")
352 | end
353 | }.to raise_exception("expected")
354 | expect(Audited.store[:audited_user]).to be_nil
355 | end
356 | end
357 | end
358 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Audited
2 | [](http://rubygems.org/gems/audited)
3 | 
4 | [](https://codeclimate.com/github/collectiveidea/audited)
5 | [](https://github.com/testdouble/standard)
6 | =======
7 |
8 | **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.
9 |
10 |
11 | Audited currently (5.6) works with Rails 7.1, 7.0, 6.1, 6.0, 5.2.
12 |
13 | For Rails 5.0 & 5.1, use gem version 5.4.3
14 | For Rails 4, use gem version 4.x
15 | For Rails 3, use gem version 3.0 or see the [3.0-stable branch](https://github.com/collectiveidea/audited/tree/3.0-stable).
16 |
17 | ## Supported Rubies
18 |
19 | Audited supports and is [tested against](https://github.com/collectiveidea/audited/actions/workflows/ci.yml) the following Ruby versions:
20 |
21 | * 2.3 (only tested on Sqlite due to testing issues with other DBs)
22 | * 2.4
23 | * 2.5
24 | * 2.6
25 | * 2.7
26 | * 3.0
27 | * 3.1
28 | * 3.2
29 |
30 | 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).
31 |
32 | ## Supported ORMs
33 |
34 | 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.
35 |
36 | ## Installation
37 |
38 | Add the gem to your Gemfile:
39 |
40 | ```ruby
41 | gem "audited"
42 | ```
43 |
44 | And if you're using ```require: false``` you must add initializers like this:
45 |
46 | ```ruby
47 | #./config/initializers/audited.rb
48 | require "audited"
49 |
50 | Audited::Railtie.initializers.each(&:run)
51 | ```
52 |
53 | Then, from your Rails app directory, create the `audits` table:
54 |
55 | ```bash
56 | $ rails generate audited:install
57 | $ rake db:migrate
58 | ```
59 |
60 | 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.
61 |
62 | 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.
63 |
64 | #### Upgrading
65 |
66 | If you're already using Audited (or acts_as_audited), your `audits` table may require additional columns. After every upgrade, please run:
67 |
68 | ```bash
69 | $ rails generate audited:upgrade
70 | $ rake db:migrate
71 | ```
72 |
73 | Upgrading will only make changes if changes are needed.
74 |
75 |
76 | ## Usage
77 |
78 | Simply call `audited` on your models:
79 |
80 | ```ruby
81 | class User < ActiveRecord::Base
82 | audited
83 | end
84 | ```
85 |
86 | By default, whenever a user is created, updated or destroyed, a new audit is created.
87 |
88 | ```ruby
89 | user = User.create!(name: "Steve")
90 | user.audits.count # => 1
91 | user.update!(name: "Ryan")
92 | user.audits.count # => 2
93 | user.destroy
94 | user.audits.count # => 3
95 | ```
96 |
97 | Audits contain information regarding what action was taken on the model and what changes were made.
98 |
99 | ```ruby
100 | user.update!(name: "Ryan")
101 | audit = user.audits.last
102 | audit.action # => "update"
103 | audit.audited_changes # => {"name"=>["Steve", "Ryan"]}
104 | ```
105 |
106 | You can get previous versions of a record by index or date, or list all
107 | revisions.
108 |
109 | ```ruby
110 | user.revisions
111 | user.revision(1)
112 | user.revision_at(Date.parse("2016-01-01"))
113 | ```
114 |
115 | ### Specifying columns
116 |
117 | By default, a new audit is created for any attribute changes. You can, however, limit the columns to be considered.
118 |
119 | ```ruby
120 | class User < ActiveRecord::Base
121 | # All fields
122 | # audited
123 |
124 | # Single field
125 | # audited only: :name
126 |
127 | # Multiple fields
128 | # audited only: [:name, :address]
129 |
130 | # All except certain fields
131 | # audited except: :password
132 | end
133 | ```
134 |
135 | ### Specifying callbacks
136 |
137 | By default, a new audit is created for any Create, Update, Touch (Rails 6+) or Destroy action. You can, however, limit the actions audited.
138 |
139 | ```ruby
140 | class User < ActiveRecord::Base
141 | # All fields and actions
142 | # audited
143 |
144 | # Single field, only audit Update and Destroy (not Create or Touch)
145 | # audited only: :name, on: [:update, :destroy]
146 | end
147 | ```
148 |
149 | You can ignore the default callbacks globally unless the callback action is specified in your model using the `:on` option. To configure default callback exclusion, put the following in an initializer file (`config/initializers/audited.rb`):
150 |
151 | ```ruby
152 | Audited.ignored_default_callbacks = [:create, :update] # ignore callbacks create and update
153 | ```
154 |
155 | ### Comments
156 |
157 | You can attach comments to each audit using an `audit_comment` attribute on your model.
158 |
159 | ```ruby
160 | user.update!(name: "Ryan", audit_comment: "Changing name, just because")
161 | user.audits.last.comment # => "Changing name, just because"
162 | ```
163 |
164 | You can optionally add the `:comment_required` option to your `audited` call to require comments for all audits.
165 |
166 | ```ruby
167 | class User < ActiveRecord::Base
168 | audited :comment_required => true
169 | end
170 | ```
171 |
172 | You can update an audit only if 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.
173 |
174 | ```ruby
175 | class User < ActiveRecord::Base
176 | audited :update_with_comment_only => false
177 | end
178 | ```
179 |
180 | ### Limiting stored audits
181 |
182 | You can limit the number of audits stored for your model. To configure limiting for all audited models, put the following in an initializer file (`config/initializers/audited.rb`):
183 |
184 | ```ruby
185 | Audited.max_audits = 10 # keep only 10 latest audits
186 | ```
187 |
188 | or customize per model:
189 |
190 | ```ruby
191 | class User < ActiveRecord::Base
192 | audited max_audits: 2
193 | end
194 | ```
195 |
196 | Whenever an object is updated or destroyed, extra audits are combined with newer ones and the old ones are destroyed.
197 |
198 | ```ruby
199 | user = User.create!(name: "Steve")
200 | user.audits.count # => 1
201 | user.update!(name: "Ryan")
202 | user.audits.count # => 2
203 | user.destroy
204 | user.audits.count # => 2
205 | ```
206 |
207 | ### Current User Tracking
208 |
209 | 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.
210 |
211 | ```ruby
212 | class PostsController < ApplicationController
213 | def create
214 | current_user # => #
215 | @post = Post.create(params[:post])
216 | @post.audits.last.user # => #
217 | end
218 | end
219 | ```
220 |
221 | To use a method other than `current_user`, put the following in an initializer file (`config/initializers/audited.rb`):
222 |
223 | ```ruby
224 | Audited.current_user_method = :authenticated_user
225 | ```
226 |
227 | Outside of a request, Audited can still record the user with the `as_user` method:
228 |
229 | ```ruby
230 | Audited.audit_class.as_user(User.find(1)) do
231 | post.update!(title: "Hello, world!")
232 | end
233 | post.audits.last.user # => #
234 | ```
235 |
236 | 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.)
237 |
238 | #### Custom Audit User
239 |
240 | You might need to use a custom auditor from time to time. This can be done by simply passing in a string:
241 |
242 | ```ruby
243 | class ApplicationController < ActionController::Base
244 | def authenticated_user
245 | if current_user
246 | current_user
247 | else
248 | 'Alexander Fleming'
249 | end
250 | end
251 | end
252 | ```
253 |
254 | `as_user` also accepts a string, which can be useful for auditing updates made in a CLI environment:
255 |
256 | ```rb
257 | Audited.audit_class.as_user("console-user-#{ENV['SSH_USER']}") do
258 | post.update_attributes!(title: "Hello, world!")
259 | end
260 | post.audits.last.user # => 'console-user-username'
261 | ```
262 |
263 | If you want to set a specific user as the auditor of the commands in a CLI environment, whether that is a string or an ActiveRecord object, you can use the following command:
264 |
265 | ```rb
266 | Audited.store[:audited_user] = "username"
267 |
268 | # or
269 |
270 | Audited.store[:audited_user] = User.find(1)
271 | ```
272 |
273 | ### Associated Audits
274 |
275 | Sometimes it's useful to associate an audit with a model other than the one being changed. For instance, given the following models:
276 |
277 | ```ruby
278 | class User < ActiveRecord::Base
279 | belongs_to :company
280 | audited
281 | end
282 |
283 | class Company < ActiveRecord::Base
284 | has_many :users
285 | end
286 | ```
287 |
288 | 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:
289 |
290 | ```ruby
291 | class User < ActiveRecord::Base
292 | belongs_to :company
293 | audited associated_with: :company
294 | end
295 |
296 | class Company < ActiveRecord::Base
297 | audited
298 | has_many :users
299 | has_associated_audits
300 | end
301 | ```
302 |
303 | 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.
304 |
305 | ```ruby
306 | company = Company.create!(name: "Collective Idea")
307 | user = company.users.create!(name: "Steve")
308 | user.update!(name: "Steve Richert")
309 | user.audits.last.associated # => #
310 | company.associated_audits.last.auditable # => #
311 | ```
312 |
313 | You can access records' own audits and associated audits in one go:
314 | ```ruby
315 | company.own_and_associated_audits
316 | ```
317 |
318 | ### Conditional auditing
319 |
320 | 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.
321 |
322 | ```ruby
323 | class User < ActiveRecord::Base
324 | audited if: :active?
325 |
326 | def active?
327 | last_login > 6.months.ago
328 | end
329 | end
330 | ```
331 |
332 | Just like in ActiveModel, you can use an inline Proc in your conditions:
333 |
334 | ```ruby
335 | class User < ActiveRecord::Base
336 | audited unless: Proc.new { |u| u.ninja? }
337 | end
338 | ```
339 |
340 | In the above case, the user will only be audited when `User#ninja` is `false`.
341 |
342 | ### Disabling auditing
343 |
344 | If you want to disable auditing temporarily doing certain tasks, there are a few
345 | methods available.
346 |
347 | To disable auditing on a save:
348 |
349 | ```ruby
350 | @user.save_without_auditing
351 | ```
352 |
353 | or:
354 |
355 | ```ruby
356 | @user.without_auditing do
357 | @user.save
358 | end
359 | ```
360 |
361 | To disable auditing on a column:
362 |
363 | ```ruby
364 | User.non_audited_columns = [:first_name, :last_name]
365 | ```
366 |
367 | To disable auditing on an entire model:
368 |
369 | ```ruby
370 | User.auditing_enabled = false
371 | ```
372 |
373 | To disable auditing on all models:
374 |
375 | ```ruby
376 | Audited.auditing_enabled = false
377 | ```
378 |
379 | If you have auditing disabled by default on your model you can enable auditing
380 | temporarily.
381 |
382 | ```ruby
383 | User.auditing_enabled = false
384 | @user.save_with_auditing
385 | ```
386 |
387 | or:
388 |
389 | ```ruby
390 | User.auditing_enabled = false
391 | @user.with_auditing do
392 | @user.save
393 | end
394 | ```
395 |
396 | ### Encrypted attributes
397 |
398 | If you're using ActiveRecord's encryption (available from Rails 7) to encrypt some attributes, Audited will automatically filter values of these attributes. No additional configuration is required. Changes to encrypted attributes will be logged as `[FILTERED]`.
399 |
400 | ```ruby
401 | class User < ActiveRecord::Base
402 | audited
403 | encrypts :password
404 | end
405 | ```
406 |
407 | ### Custom `Audit` model
408 |
409 | If you want to extend or modify the audit model, create a new class that
410 | inherits from `Audited::Audit`:
411 | ```ruby
412 | class CustomAudit < Audited::Audit
413 | def some_custom_behavior
414 | "Hiya!"
415 | end
416 | end
417 | ```
418 | Then set it in an initializer:
419 | ```ruby
420 | # config/initializers/audited.rb
421 |
422 | Audited.config do |config|
423 | config.audit_class = "CustomAudit"
424 | end
425 | ```
426 |
427 | ### Enum Storage
428 |
429 | In 4.10, the default behavior for enums changed from storing the value synthesized by Rails to the value stored in the DB. You can restore the previous behavior by setting the store_synthesized_enums configuration value:
430 |
431 | ```ruby
432 | # config/initializers/audited.rb
433 |
434 | Audited.store_synthesized_enums = true
435 | ```
436 |
437 | ## Support
438 |
439 | You can find documentation at: https://www.rubydoc.info/gems/audited
440 |
441 | Or join the [mailing list](http://groups.google.com/group/audited) to get help or offer suggestions.
442 |
443 | ## Contributing
444 |
445 | 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:
446 |
447 | * Use prerelease versions of Audited.
448 | * [Report bugs](https://github.com/collectiveidea/audited/issues).
449 | * Fix bugs and submit [pull requests](http://github.com/collectiveidea/audited/pulls).
450 | * Write, clarify or fix documentation.
451 | * Refactor code.
452 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Audited ChangeLog
2 |
3 | ### 5.6.0 (2024-04-05)
4 |
5 | - Removed support for Rails 5.0 and 5.1.
6 | - Replace RequestStore with ActiveSupport::CurrentAttributes - @punkisdead
7 | [#702](https://github.com/collectiveidea/audited/pull/702)
8 |
9 | ### 5.5.0 (2024-04-02)
10 |
11 | - Bad release. Same code as 5.4.1. Use 5.6.0 for updated features.
12 |
13 | ### 5.4.3 (2024-01-11)
14 |
15 | - Ignore readonly columns in audit - @sriddbs
16 | [#692](https://github.com/collectiveidea/audited/pull/692)
17 | - Robustify Rails version checks - @blaet
18 | [#689](https://github.com/collectiveidea/audited/pull/689)
19 | - Ignore callbacks if not specifed on the model
20 | [#679](https://github.com/collectiveidea/audited/pull/679)
21 |
22 | ## 5.4.2 (2023-11-30)
23 |
24 | - Revert replacing RequetStore with ActiveSupport::CurrentAttributes until it is fully tested.
25 |
26 | ## 5.4.1 (2023-11-30)
27 |
28 | - Replace RequestStore with ActiveSupport::CurrentAttributes - @the-spectator
29 | [#673](https://github.com/collectiveidea/audited/pull/673/)
30 | - Don't require railtie when used outside of Rails - @nicduke38degrees
31 | [#665](https://github.com/collectiveidea/audited/pull/665)
32 |
33 | ## 5.4.0 (2023-09-30)
34 |
35 | - Add Rails 7.1 support - @yuki24
36 | [#686](https://github.com/collectiveidea/audited/pull/686)
37 |
38 | ## 5.3.3 (2023-03-24)
39 |
40 | - Use RequestStore instead of Thread.current for thread-safe requests - @tiagocassio
41 | [#669](https://github.com/collectiveidea/audited/pull/669)
42 | - Clean up Touch audits - @mcyoung, @akostadinov
43 | [#668](https://github.com/collectiveidea/audited/pull/668)
44 |
45 | ## 5.3.2 (2023-02-22)
46 |
47 | - Touch audit bug fixes - @mcyoung
48 | [#662](https://github.com/collectiveidea/audited/pull/662)
49 |
50 | ## 5.3.1 (2023-02-21)
51 |
52 | - Ensure touch support doesn't cause double audits - @mcyoung
53 | [#660](https://github.com/collectiveidea/audited/pull/660)
54 | - Testing Improvements - @vlad-psh
55 | [#628](https://github.com/collectiveidea/audited/pull/628)
56 | - Testing Improvements - @mcyoung
57 | [#658](https://github.com/collectiveidea/audited/pull/658)
58 |
59 | ## 5.3.0 (2023-02-14)
60 |
61 | - Audit touch calls - @mcyoung
62 | [#657](https://github.com/collectiveidea/audited/pull/657)
63 | - Allow using with Padrino and other non-Rails projects - @nicduke38degrees
64 | [#655](https://github.com/collectiveidea/audited/pull/655)
65 | - Testing updates - @jdufresne
66 | [#652](https://github.com/collectiveidea/audited/pull/652)
67 | [#653](https://github.com/collectiveidea/audited/pull/653)
68 |
69 | ## 5.2.0 (2023-01-23)
70 |
71 | Improved
72 |
73 | - config.audit_class can take a string or constant - @rocket-turtle
74 | Fixes overzealous change in 5.1.0 where it only took a string.
75 | [#648](https://github.com/collectiveidea/audited/pull/648)
76 | - README link fix - @jeremiahlukus
77 | [#646](https://github.com/collectiveidea/audited/pull/646)
78 | - Typo fix in GitHub Actions - @jdufresne
79 | [#644](https://github.com/collectiveidea/audited/pull/644)
80 |
81 | ## 5.1.0 (2022-12-23)
82 |
83 | Changed
84 |
85 | - config.audit_class takes a string - @simmerz
86 | [#609](https://github.com/collectiveidea/audited/pull/609)
87 | - Filter encrypted attributes automatically - @vlad-psh
88 | [#630](https://github.com/collectiveidea/audited/pull/630)
89 |
90 | Improved
91 |
92 | - README improvements - @jess, @mstroming
93 | [#605](https://github.com/collectiveidea/audited/pull/605)
94 | [#640](https://github.com/collectiveidea/audited/issues/640)
95 | - Ignore deadlocks in concurrent audit combinations - @Crammaman
96 | [#621](https://github.com/collectiveidea/audited/pull/621)
97 | - Fix timestamped_migrations deprecation warning - @shouichi
98 | [#624](https://github.com/collectiveidea/audited/pull/624)
99 | - Ensure audits are re-enabled after blocks - @dcorlett
100 | [#632](https://github.com/collectiveidea/audited/pull/632)
101 | - Replace raw string where clause with query methods - @macowie
102 | [#642](https://github.com/collectiveidea/audited/pull/642)
103 | - Test against more Ruby/Rails Versions - @enomotodev, @danielmorrison
104 | [#610](https://github.com/collectiveidea/audited/pull/610)
105 | [#643](https://github.com/collectiveidea/audited/pull/643)
106 |
107 | ## 5.0.2 (2021-09-16)
108 |
109 | Added
110 |
111 | - Relax ActiveRecord version constraint to support Rails 7
112 | [#597](https://github.com/collectiveidea/audited/pull/597)
113 |
114 | Improved
115 |
116 | - Improve loading - @mvastola
117 | [#592](https://github.com/collectiveidea/audited/pull/592)
118 | - Update README - @danirod, @clement1234
119 | [#596](https://github.com/collectiveidea/audited/pull/596)
120 | [#594](https://github.com/collectiveidea/audited/pull/594)
121 |
122 |
123 | ## 5.0.1 (2021-06-11)
124 |
125 | Improved
126 |
127 | - Don't load associated model when auditing is disabled - @nut4k1
128 | [#584](https://github.com/collectiveidea/audited/pull/584)
129 |
130 | ## 5.0.0 (2021-06-10)
131 |
132 | Improved
133 |
134 | - Fixes an issue where array attributes were not deserialized properly - @cfeckardt, @yuki24
135 | [#448](https://github.com/collectiveidea/audited/pull/448)
136 | [#576](https://github.com/collectiveidea/audited/pull/576)
137 | - Improve error message on audit_comment and allow for i18n override - @james
138 | [#523](https://github.com/collectiveidea/audited/pull/523/)
139 | - Don't require a comment if only non-audited fields are changed - @james
140 | [#522](https://github.com/collectiveidea/audited/pull/522/)
141 | - Readme updates - @gourshete
142 | [#525](https://github.com/collectiveidea/audited/pull/525)
143 | - Allow restoring previous enum behavior with flag - @travisofthenorth
144 | [#526](https://github.com/collectiveidea/audited/pull/526)
145 | - Follow Rails Autoloading conventions - @duncanjbrown
146 | [#532](https://github.com/collectiveidea/audited/pull/532)
147 | - Fix own_and_associated_audits for STI Models - @eric-hemasystems
148 | [#533](https://github.com/collectiveidea/audited/pull/533)
149 | - Rails 6.1 Improvements - @okuramasafumi, @marcrohloff
150 | [#563](https://github.com/collectiveidea/audited/pull/563)
151 | [#544](https://github.com/collectiveidea/audited/pull/544)
152 | - Use Thread local variables instead of Fibers - @arathunku
153 | [#568](https://github.com/collectiveidea/audited/pull/568)
154 |
155 | Changed
156 |
157 | - Drop support for Rails 4 - @travisofthenorth
158 | [#527](https://github.com/collectiveidea/audited/pull/527)
159 |
160 | ## 4.10.0 (2021-01-07)
161 |
162 | Added
163 |
164 | - Add redacted option
165 | [#485](https://github.com/collectiveidea/audited/pull/485)
166 | - Rails 6.1. support
167 | [#554](https://github.com/collectiveidea/audited/pull/554)
168 | [#559](https://github.com/collectiveidea/audited/pull/559)
169 |
170 | Improved
171 |
172 | - Avoid extra query on first audit version
173 | [#513](https://github.com/collectiveidea/audited/pull/513)
174 |
175 |
176 | ## 4.9.0 (2019-07-17)
177 |
178 | Breaking changes
179 |
180 | - removed block support for `Audit.reconstruct_attributes`
181 | [#437](https://github.com/collectiveidea/audited/pull/437)
182 | - removed `audited_columns`, `non_audited_columns`, `auditing_enabled=` instance methods,
183 | use class methods instead
184 | [#424](https://github.com/collectiveidea/audited/pull/424)
185 | - removed rails 4.1 and 4.0 support
186 | [#431](https://github.com/collectiveidea/audited/pull/431)
187 |
188 | Added
189 |
190 | - Add `with_auditing` methods to enable temporarily
191 | [#502](https://github.com/collectiveidea/audited/pull/502)
192 | - Add `update_with_comment_only` option to control audit creation with only comments
193 | [#327](https://github.com/collectiveidea/audited/pull/327)
194 | - Support for Rails 6.0 and Ruby 2.6
195 | [#494](https://github.com/collectiveidea/audited/pull/494)
196 |
197 | Changed
198 |
199 | - None
200 |
201 | Fixed
202 |
203 | - Ensure enum changes are stored consistently
204 | [#429](https://github.com/collectiveidea/audited/pull/429)
205 |
206 | ## 4.8.0 (2018-08-19)
207 |
208 | Breaking changes
209 |
210 | - None
211 |
212 | Added
213 |
214 | - Add ability to globally disable auditing
215 | [#426](https://github.com/collectiveidea/audited/pull/426)
216 | - Add `own_and_associated_audits` method to auditable models
217 | [#428](https://github.com/collectiveidea/audited/pull/428)
218 | - Ability to nest `as_user` within itself
219 | [#450](https://github.com/collectiveidea/audited/pull/450)
220 | - Private methods can now be used for conditional auditing
221 | [#454](https://github.com/collectiveidea/audited/pull/454)
222 |
223 | Changed
224 |
225 | - Add version to `auditable_index`
226 | [#427](https://github.com/collectiveidea/audited/pull/427)
227 | - Rename audited resource revision `version` attribute to `audit_version` and deprecate `version` attribute
228 | [#443](https://github.com/collectiveidea/audited/pull/443)
229 |
230 | Fixed
231 |
232 | - None
233 |
234 | ## 4.7.1 (2018-04-10)
235 |
236 | Breaking changes
237 |
238 | - None
239 |
240 | Added
241 |
242 | - None
243 |
244 | Changed
245 |
246 | - None
247 |
248 | Fixed
249 |
250 | - Allow use with Rails 5.2 final
251 |
252 | ## 4.7.0 (2018-03-14)
253 |
254 | Breaking changes
255 |
256 | - None
257 |
258 | Added
259 |
260 | - Add `inverse_of: auditable` definition to audit relation
261 | [#413](https://github.com/collectiveidea/audited/pull/413)
262 | - Add functionality to conditionally audit models
263 | [#414](https://github.com/collectiveidea/audited/pull/414)
264 | - Allow limiting number of audits stored
265 | [#405](https://github.com/collectiveidea/audited/pull/405)
266 |
267 | Changed
268 |
269 | - Reduced db calls in `#revisions` method
270 | [#402](https://github.com/collectiveidea/audited/pull/402)
271 | [#403](https://github.com/collectiveidea/audited/pull/403)
272 | - Update supported Ruby and Rails versions
273 | [#404](https://github.com/collectiveidea/audited/pull/404)
274 | [#409](https://github.com/collectiveidea/audited/pull/409)
275 | [#415](https://github.com/collectiveidea/audited/pull/415)
276 | [#416](https://github.com/collectiveidea/audited/pull/416)
277 |
278 | Fixed
279 |
280 | - Ensure that `on` and `except` options jive with `comment_required: true`
281 | [#419](https://github.com/collectiveidea/audited/pull/419)
282 | - Fix RSpec matchers
283 | [#420](https://github.com/collectiveidea/audited/pull/420)
284 |
285 | ## 4.6.0 (2018-01-10)
286 |
287 | Breaking changes
288 |
289 | - None
290 |
291 | Added
292 |
293 | - Add functionality to undo specific audit
294 | [#381](https://github.com/collectiveidea/audited/pull/381)
295 |
296 | Changed
297 |
298 | - Removed duplicate declaration of `non_audited_columns` method
299 | [#365](https://github.com/collectiveidea/audited/pull/365)
300 | - Updated `audited_changes` calculation to support Rails>=5.1 change syntax
301 | [#377](https://github.com/collectiveidea/audited/pull/377)
302 | - Improve index ordering for polymorphic indexes
303 | [#385](https://github.com/collectiveidea/audited/pull/385)
304 | - Update CI to test on newer versions of Ruby and Rails
305 | [#386](https://github.com/collectiveidea/audited/pull/386)
306 | [#387](https://github.com/collectiveidea/audited/pull/387)
307 | [#388](https://github.com/collectiveidea/audited/pull/388)
308 | - Simplify `audited_columns` calculation
309 | [#391](https://github.com/collectiveidea/audited/pull/391)
310 | - Simplify `audited_changes` calculation
311 | [#389](https://github.com/collectiveidea/audited/pull/389)
312 | - Normalize options passed to `audited` method
313 | [#397](https://github.com/collectiveidea/audited/pull/397)
314 |
315 | Fixed
316 |
317 | - Fixed typo in rspec causing incorrect test failure
318 | [#360](https://github.com/collectiveidea/audited/pull/360)
319 | - Allow running specs using rake
320 | [#390](https://github.com/collectiveidea/audited/pull/390)
321 | - Passing an invalid version to `revision` returns `nil` instead of last version
322 | [#384](https://github.com/collectiveidea/audited/pull/384)
323 | - Fix duplicate deceleration warnings
324 | [#399](https://github.com/collectiveidea/audited/pull/399)
325 |
326 |
327 | ## 4.5.0 (2017-05-22)
328 |
329 | Breaking changes
330 |
331 | - None
332 |
333 | Added
334 |
335 | - Support for `user_id` column to be a `uuid` type
336 | [#333](https://github.com/collectiveidea/audited/pull/333)
337 |
338 | Fixed
339 |
340 | - Fix retrieval of user from controller when populated in before callbacks
341 | [#336](https://github.com/collectiveidea/audited/issues/336)
342 | - Fix column type check in serializer for Oracle DB adapter
343 | [#335](https://github.com/collectiveidea/audited/pull/335)
344 | - Fix `non_audited_columns` to allow symbol names
345 | [#351](https://github.com/collectiveidea/audited/pull/351)
346 |
347 | ## 4.4.1 (2017-03-29)
348 |
349 | Fixed
350 |
351 | - Fix ActiveRecord gem dependency to permit 5.1
352 | [#332](https://github.com/collectiveidea/audited/pull/332)
353 |
354 | ## 4.4.0 (2017-03-29)
355 |
356 | Breaking changes
357 |
358 | - None
359 |
360 | Added
361 |
362 | - Support for `audited_changes` to be a `json` or `jsonb` column in PostgreSQL
363 | [#216](https://github.com/collectiveidea/audited/issues/216)
364 | - Allow `Audited::Audit` to be subclassed by configuring `Audited.audit_class`
365 | [#314](https://github.com/collectiveidea/audited/issues/314)
366 | - Support for Ruby on Rails 5.1
367 | [#329](https://github.com/collectiveidea/audited/issues/329)
368 | - Support for Ruby 2.4
369 | [#329](https://github.com/collectiveidea/audited/issues/329)
370 |
371 | Changed
372 |
373 | - Remove rails-observer dependency
374 | [#325](https://github.com/collectiveidea/audited/issues/325)
375 | - Undeprecated `Audited.audit_class` reader
376 | [#314](https://github.com/collectiveidea/audited/issues/314)
377 |
378 | Fixed
379 |
380 | - SQL error in Rails Conditional GET (304 caching)
381 | [#295](https://github.com/collectiveidea/audited/pull/295)
382 | - Fix missing non_audited_columns= configuration setter
383 | [#320](https://github.com/collectiveidea/audited/issues/320)
384 | - Fix migration generators to specify AR migration version
385 | [#329](https://github.com/collectiveidea/audited/issues/329)
386 |
387 | ## 4.3.0 (2016-09-17)
388 |
389 | Breaking changes
390 |
391 | - None
392 |
393 | Added
394 |
395 | - Support singular arguments for options: `on` and `only`
396 |
397 | Fixed
398 |
399 | - Fix auditing instance attributes if "only" option specified
400 | - Allow private / protected callback declarations
401 | - Do not eagerly connect to database
402 |
403 | ## 4.2.2 (2016-08-01)
404 |
405 | - Correct auditing_enabled for STI models
406 | - Properly set table name for mongomapper
407 |
408 | ## 4.2.1 (2016-07-29)
409 |
410 | - Fix bug when only: is a single field.
411 | - update gemspec to use mongomapper 0.13
412 | - sweeper need not run observer for mongomapper
413 | - Make temporary disabling of auditing threadsafe
414 | - Centralize `Audited.store` as thread safe variable store
415 |
416 | ## 4.2.0 (2015-03-31)
417 |
418 | Not yet documented.
419 |
420 | ## 4.0.0 (2014-09-04)
421 |
422 | Not yet documented.
423 |
424 | ## 4.0.0.rc1 (2014-07-30)
425 |
426 | Not yet documented.
427 |
428 | ## 3.0.0 (2012-09-25)
429 |
430 | Not yet documented.
431 |
432 | ## 3.0.0.rc2 (2012-07-09)
433 |
434 | Not yet documented.
435 |
436 | ## 3.0.0.rc1 (2012-04-25)
437 |
438 | Not yet documented.
439 |
440 | ## 2012-04-10
441 |
442 | - Add Audit scopes for creates, updates and destroys [chriswfx]
443 |
444 | ## 2011-10-25
445 |
446 | - Made ignored_attributes configurable [senny]
447 |
448 | ## 2011-09-09
449 |
450 | - Rails 3.x support
451 | - Support for associated audits
452 | - Support for remote IP address storage
453 | - Plenty of bug fixes and refactoring
454 | - [kennethkalmer, ineu, PatrickMa, jrozner, dwarburton, bsiggelkow, dgm]
455 |
456 | ## 2009-01-27
457 |
458 | - Store old and new values for updates, and store all attributes on destroy.
459 | - Refactored revisioning methods to work as expected
460 |
461 | ## 2008-10-10
462 |
463 | - changed to make it work in development mode
464 |
465 | ## 2008-09-24
466 |
467 | - Add ability to record parent record of the record being audited [Kenneth Kalmer]
468 |
469 | ## 2008-04-19
470 |
471 | - refactored to make compatible with dirty tracking in edge rails
472 | and to stop storing both old and new values in a single audit
473 |
474 | ## 2008-04-18
475 |
476 | - Fix NoMethodError when trying to access the :previous revision
477 | on a model that doesn't have previous revisions [Alex Soto]
478 |
479 | ## 2008-03-21
480 |
481 | - added #changed_attributes to get access to the changes before a
482 | save [Chris Parker]
483 |
484 | ## 2007-12-16
485 |
486 | - Added #revision_at for retrieving a revision from a specific
487 | time [Jacob Atzen]
488 |
489 | ## 2007-12-16
490 |
491 | - Fix error when getting revision from audit with no changes
492 | [Geoffrey Wiseman]
493 |
494 | ## 2007-12-16
495 |
496 | - Remove dependency on acts_as_list
497 |
498 | ## 2007-06-17
499 |
500 | - Added support getting previous revisions
501 |
502 | ## 2006-11-17
503 |
504 | - Replaced use of singleton User.current_user with cache sweeper
505 | implementation for auditing the user that made the change
506 |
507 | ## 2006-11-17
508 |
509 | - added migration generator
510 |
511 | ## 2006-08-14
512 |
513 | - incorporated changes from Michael Schuerig to write_attribute
514 | that saves the new value after every change and not just the
515 | first, and performs proper type-casting before doing comparisons
516 |
517 | ## 2006-08-14
518 |
519 | - The "changes" are now saved as a serialized hash
520 |
521 | ## 2006-07-21
522 |
523 | - initial version
524 |
--------------------------------------------------------------------------------
/lib/audited/auditor.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Audited
4 | # Specify this act if you want changes to your model to be saved in an
5 | # audit table. This assumes there is an audits table ready.
6 | #
7 | # class User < ActiveRecord::Base
8 | # audited
9 | # end
10 | #
11 | # To store an audit comment set model.audit_comment to your comment before
12 | # a create, update or destroy operation.
13 | #
14 | # See Audited::Auditor::ClassMethods#audited
15 | # for configuration options
16 | module Auditor # :nodoc:
17 | extend ActiveSupport::Concern
18 |
19 | CALLBACKS = [:audit_create, :audit_update, :audit_destroy]
20 |
21 | module ClassMethods
22 | # == Configuration options
23 | #
24 | #
25 | # * +only+ - Only audit the given attributes
26 | # * +except+ - Excludes fields from being saved in the audit log.
27 | # By default, Audited will audit all but these fields:
28 | #
29 | # [self.primary_key, inheritance_column, 'lock_version', 'created_at', 'updated_at']
30 | # You can add to those by passing one or an array of fields to skip.
31 | #
32 | # class User < ActiveRecord::Base
33 | # audited except: :password
34 | # end
35 | #
36 | # * +require_comment+ - Ensures that audit_comment is supplied before
37 | # any create, update or destroy operation.
38 | # * +max_audits+ - Limits the number of stored audits.
39 |
40 | # * +redacted+ - Changes to these fields will be logged, but the values
41 | # will not. This is useful, for example, if you wish to audit when a
42 | # password is changed, without saving the actual password in the log.
43 | # To store values as something other than '[REDACTED]', pass an argument
44 | # to the redaction_value option.
45 | #
46 | # class User < ActiveRecord::Base
47 | # audited redacted: :password, redaction_value: SecureRandom.uuid
48 | # end
49 | #
50 | # * +if+ - Only audit the model when the given function returns true
51 | # * +unless+ - Only audit the model when the given function returns false
52 | #
53 | # class User < ActiveRecord::Base
54 | # audited :if => :active?
55 | #
56 | # def active?
57 | # self.status == 'active'
58 | # end
59 | # end
60 | #
61 | def audited(options = {})
62 | # don't allow multiple calls
63 | return if included_modules.include?(Audited::Auditor::AuditedInstanceMethods)
64 |
65 | extend Audited::Auditor::AuditedClassMethods
66 | include Audited::Auditor::AuditedInstanceMethods
67 |
68 | class_attribute :audit_associated_with, instance_writer: false
69 | class_attribute :audited_options, instance_writer: false
70 | attr_accessor :audit_version, :audit_comment
71 |
72 | self.audited_options = options
73 | normalize_audited_options
74 |
75 | self.audit_associated_with = audited_options[:associated_with]
76 |
77 | if audited_options[:comment_required]
78 | validate :presence_of_audit_comment
79 | before_destroy :require_comment if audited_options[:on].include?(:destroy)
80 | end
81 |
82 | has_many :audits, -> { order(version: :asc) }, as: :auditable, class_name: Audited.audit_class.name, inverse_of: :auditable
83 | Audited.audit_class.audited_class_names << to_s
84 |
85 | after_create :audit_create if audited_options[:on].include?(:create)
86 | before_update :audit_update if audited_options[:on].include?(:update)
87 | after_touch :audit_touch if audited_options[:on].include?(:touch) && ::ActiveRecord::VERSION::MAJOR >= 6
88 | before_destroy :audit_destroy if audited_options[:on].include?(:destroy)
89 |
90 | # Define and set after_audit and around_audit callbacks. This might be useful if you want
91 | # to notify a party after the audit has been created or if you want to access the newly-created
92 | # audit.
93 | define_callbacks :audit
94 | set_callback :audit, :after, :after_audit, if: lambda { respond_to?(:after_audit, true) }
95 | set_callback :audit, :around, :around_audit, if: lambda { respond_to?(:around_audit, true) }
96 |
97 | enable_auditing
98 | end
99 |
100 | def has_associated_audits
101 | has_many :associated_audits, as: :associated, class_name: Audited.audit_class.name
102 | end
103 | end
104 |
105 | module AuditedInstanceMethods
106 | REDACTED = "[REDACTED]"
107 |
108 | # Temporarily turns off auditing while saving.
109 | def save_without_auditing
110 | without_auditing { save }
111 | end
112 |
113 | # Executes the block with the auditing callbacks disabled.
114 | #
115 | # @foo.without_auditing do
116 | # @foo.save
117 | # end
118 | #
119 | def without_auditing(&block)
120 | self.class.without_auditing(&block)
121 | end
122 |
123 | # Temporarily turns on auditing while saving.
124 | def save_with_auditing
125 | with_auditing { save }
126 | end
127 |
128 | # Executes the block with the auditing callbacks enabled.
129 | #
130 | # @foo.with_auditing do
131 | # @foo.save
132 | # end
133 | #
134 | def with_auditing(&block)
135 | self.class.with_auditing(&block)
136 | end
137 |
138 | # Gets an array of the revisions available
139 | #
140 | # user.revisions.each do |revision|
141 | # user.name
142 | # user.version
143 | # end
144 | #
145 | def revisions(from_version = 1)
146 | return [] unless audits.from_version(from_version).exists?
147 |
148 | all_audits = audits.select([:audited_changes, :version, :action]).to_a
149 | targeted_audits = all_audits.select { |audit| audit.version >= from_version }
150 |
151 | previous_attributes = reconstruct_attributes(all_audits - targeted_audits)
152 |
153 | targeted_audits.map do |audit|
154 | previous_attributes.merge!(audit.new_attributes)
155 | revision_with(previous_attributes.merge!(version: audit.version))
156 | end
157 | end
158 |
159 | # Get a specific revision specified by the version number, or +:previous+
160 | # Returns nil for versions greater than revisions count
161 | def revision(version)
162 | if version == :previous || audits.last.version >= version
163 | revision_with Audited.audit_class.reconstruct_attributes(audits_to(version))
164 | end
165 | end
166 |
167 | # Find the oldest revision recorded prior to the date/time provided.
168 | def revision_at(date_or_time)
169 | audits = self.audits.up_until(date_or_time)
170 | revision_with Audited.audit_class.reconstruct_attributes(audits) unless audits.empty?
171 | end
172 |
173 | # List of attributes that are audited.
174 | def audited_attributes
175 | audited_attributes = attributes.except(*self.class.non_audited_columns)
176 | audited_attributes = redact_values(audited_attributes)
177 | audited_attributes = filter_encrypted_attrs(audited_attributes)
178 | normalize_enum_changes(audited_attributes)
179 | end
180 |
181 | # Returns a list combined of record audits and associated audits.
182 | def own_and_associated_audits
183 | Audited.audit_class.unscoped.where(auditable: self)
184 | .or(Audited.audit_class.unscoped.where(associated: self))
185 | .order(created_at: :desc)
186 | end
187 |
188 | # Combine multiple audits into one.
189 | def combine_audits(audits_to_combine)
190 | combine_target = audits_to_combine.last
191 | combine_target.audited_changes = audits_to_combine.pluck(:audited_changes).reduce(&:merge)
192 | combine_target.comment = "#{combine_target.comment}\nThis audit is the result of multiple audits being combined."
193 |
194 | transaction do
195 | begin
196 | combine_target.save!
197 | audits_to_combine.unscope(:limit).where("version < ?", combine_target.version).delete_all
198 | rescue ActiveRecord::Deadlocked
199 | # Ignore Deadlocks, if the same record is getting its old audits combined more than once at the same time then
200 | # both combining operations will be the same. Ignoring this error allows one of the combines to go through successfully.
201 | end
202 | end
203 | end
204 |
205 | protected
206 |
207 | def revision_with(attributes)
208 | dup.tap do |revision|
209 | revision.id = id
210 | revision.send :instance_variable_set, "@new_record", destroyed?
211 | revision.send :instance_variable_set, "@persisted", !destroyed?
212 | revision.send :instance_variable_set, "@readonly", false
213 | revision.send :instance_variable_set, "@destroyed", false
214 | revision.send :instance_variable_set, "@_destroyed", false
215 | revision.send :instance_variable_set, "@marked_for_destruction", false
216 | Audited.audit_class.assign_revision_attributes(revision, attributes)
217 |
218 | # Remove any association proxies so that they will be recreated
219 | # and reference the correct object for this revision. The only way
220 | # to determine if an instance variable is a proxy object is to
221 | # see if it responds to certain methods, as it forwards almost
222 | # everything to its target.
223 | revision.instance_variables.each do |ivar|
224 | proxy = revision.instance_variable_get ivar
225 | if !proxy.nil? && proxy.respond_to?(:proxy_respond_to?)
226 | revision.instance_variable_set ivar, nil
227 | end
228 | end
229 | end
230 | end
231 |
232 | private
233 |
234 | def audited_changes(for_touch: false, exclude_readonly_attrs: false)
235 | all_changes = if for_touch
236 | previous_changes
237 | elsif respond_to?(:changes_to_save)
238 | changes_to_save
239 | else
240 | changes
241 | end
242 |
243 | all_changes = all_changes.except(*self.class.readonly_attributes.to_a) if exclude_readonly_attrs
244 |
245 | filtered_changes = \
246 | if audited_options[:only].present?
247 | all_changes.slice(*self.class.audited_columns)
248 | else
249 | all_changes.except(*self.class.non_audited_columns)
250 | end
251 |
252 | filtered_changes = normalize_enum_changes(filtered_changes)
253 |
254 | if for_touch && (last_audit = audits.last&.audited_changes)
255 | filtered_changes.reject! do |k, v|
256 | last_audit[k].to_json == v.to_json ||
257 | last_audit[k].to_json == v[1].to_json
258 | end
259 | end
260 |
261 | filtered_changes = redact_values(filtered_changes)
262 | filtered_changes = filter_encrypted_attrs(filtered_changes)
263 | filtered_changes.to_hash
264 | end
265 |
266 | def normalize_enum_changes(changes)
267 | return changes if Audited.store_synthesized_enums
268 |
269 | self.class.defined_enums.each do |name, values|
270 | if changes.has_key?(name)
271 | changes[name] = \
272 | if changes[name].is_a?(Array)
273 | changes[name].map { |v| values[v] }
274 | elsif rails_below?("5.0")
275 | changes[name]
276 | else
277 | values[changes[name]]
278 | end
279 | end
280 | end
281 | changes
282 | end
283 |
284 | def redact_values(filtered_changes)
285 | filter_attr_values(
286 | audited_changes: filtered_changes,
287 | attrs: Array(audited_options[:redacted]).map(&:to_s),
288 | placeholder: audited_options[:redaction_value] || REDACTED
289 | )
290 | end
291 |
292 | def filter_encrypted_attrs(filtered_changes)
293 | filter_attr_values(
294 | audited_changes: filtered_changes,
295 | attrs: respond_to?(:encrypted_attributes) ? Array(encrypted_attributes).map(&:to_s) : []
296 | )
297 | end
298 |
299 | # Replace values for given attrs to a placeholder and return modified hash
300 | #
301 | # @param audited_changes [Hash] Hash of changes to be saved to audited version record
302 | # @param attrs [Array] Array of attrs, values of which will be replaced to placeholder value
303 | # @param placeholder [String] Placeholder to replace original attr values
304 | def filter_attr_values(audited_changes: {}, attrs: [], placeholder: "[FILTERED]")
305 | attrs.each do |attr|
306 | next unless audited_changes.key?(attr)
307 |
308 | changes = audited_changes[attr]
309 | values = changes.is_a?(Array) ? changes.map { placeholder } : placeholder
310 |
311 | audited_changes[attr] = values
312 | end
313 |
314 | audited_changes
315 | end
316 |
317 | def rails_below?(rails_version)
318 | Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new(rails_version)
319 | end
320 |
321 | def audits_to(version = nil)
322 | if version == :previous
323 | version = if audit_version
324 | audit_version - 1
325 | else
326 | previous = audits.descending.offset(1).first
327 | previous ? previous.version : 1
328 | end
329 | end
330 | audits.to_version(version)
331 | end
332 |
333 | def audit_create
334 | write_audit(action: "create", audited_changes: audited_attributes,
335 | comment: audit_comment)
336 | end
337 |
338 | def audit_update
339 | unless (changes = audited_changes(exclude_readonly_attrs: true)).empty? && (audit_comment.blank? || audited_options[:update_with_comment_only] == false)
340 | write_audit(action: "update", audited_changes: changes,
341 | comment: audit_comment)
342 | end
343 | end
344 |
345 | def audit_touch
346 | unless (changes = audited_changes(for_touch: true, exclude_readonly_attrs: true)).empty?
347 | write_audit(action: "update", audited_changes: changes,
348 | comment: audit_comment)
349 | end
350 | end
351 |
352 | def audit_destroy
353 | unless new_record?
354 | write_audit(action: "destroy", audited_changes: audited_attributes,
355 | comment: audit_comment)
356 | end
357 | end
358 |
359 | def write_audit(attrs)
360 | self.audit_comment = nil
361 |
362 | if auditing_enabled
363 | attrs[:associated] = send(audit_associated_with) unless audit_associated_with.nil?
364 |
365 | run_callbacks(:audit) {
366 | audit = audits.create(attrs)
367 | combine_audits_if_needed if attrs[:action] != "create"
368 | audit
369 | }
370 | end
371 | end
372 |
373 | def presence_of_audit_comment
374 | if comment_required_state?
375 | errors.add(:audit_comment, :blank) unless audit_comment.present?
376 | end
377 | end
378 |
379 | def comment_required_state?
380 | auditing_enabled &&
381 | audited_changes.present? &&
382 | ((audited_options[:on].include?(:create) && new_record?) ||
383 | (audited_options[:on].include?(:update) && persisted? && changed?))
384 | end
385 |
386 | def combine_audits_if_needed
387 | max_audits = evaluate_max_audits
388 |
389 | if max_audits && (extra_count = audits.count - max_audits) > 0
390 | audits_to_combine = audits.limit(extra_count + 1)
391 | combine_audits(audits_to_combine)
392 | end
393 | end
394 |
395 | def evaluate_max_audits
396 | max_audits = case (option = audited_options[:max_audits])
397 | when Proc then option.call
398 | when Symbol then send(option)
399 | else
400 | option
401 | end
402 |
403 | Integer(max_audits).abs if max_audits
404 | end
405 |
406 | def require_comment
407 | if auditing_enabled && audit_comment.blank?
408 | errors.add(:audit_comment, :blank)
409 | throw(:abort)
410 | end
411 | end
412 |
413 | CALLBACKS.each do |attr_name|
414 | alias_method "#{attr_name}_callback".to_sym, attr_name
415 | end
416 |
417 | def auditing_enabled
418 | run_conditional_check(audited_options[:if]) &&
419 | run_conditional_check(audited_options[:unless], matching: false) &&
420 | self.class.auditing_enabled
421 | end
422 |
423 | def run_conditional_check(condition, matching: true)
424 | return true if condition.blank?
425 | return condition.call(self) == matching if condition.respond_to?(:call)
426 | return send(condition) == matching if respond_to?(condition.to_sym, true)
427 |
428 | true
429 | end
430 |
431 | def reconstruct_attributes(audits)
432 | attributes = {}
433 | audits.each { |audit| attributes.merge!(audit.new_attributes) }
434 | attributes
435 | end
436 | end
437 |
438 | module AuditedClassMethods
439 | # Returns an array of columns that are audited. See non_audited_columns
440 | def audited_columns
441 | @audited_columns ||= column_names - non_audited_columns
442 | end
443 |
444 | # We have to calculate this here since column_names may not be available when `audited` is called
445 | def non_audited_columns
446 | @non_audited_columns ||= calculate_non_audited_columns
447 | end
448 |
449 | def non_audited_columns=(columns)
450 | @audited_columns = nil # reset cached audited columns on assignment
451 | @non_audited_columns = columns.map(&:to_s)
452 | end
453 |
454 | # Executes the block with auditing disabled.
455 | #
456 | # Foo.without_auditing do
457 | # @foo.save
458 | # end
459 | #
460 | def without_auditing
461 | auditing_was_enabled = class_auditing_enabled
462 | disable_auditing
463 | yield
464 | ensure
465 | enable_auditing if auditing_was_enabled
466 | end
467 |
468 | # Executes the block with auditing enabled.
469 | #
470 | # Foo.with_auditing do
471 | # @foo.save
472 | # end
473 | #
474 | def with_auditing
475 | auditing_was_enabled = class_auditing_enabled
476 | enable_auditing
477 | yield
478 | ensure
479 | disable_auditing unless auditing_was_enabled
480 | end
481 |
482 | def disable_auditing
483 | self.auditing_enabled = false
484 | end
485 |
486 | def enable_auditing
487 | self.auditing_enabled = true
488 | end
489 |
490 | # All audit operations during the block are recorded as being
491 | # made by +user+. This is not model specific, the method is a
492 | # convenience wrapper around
493 | # @see Audit#as_user.
494 | def audit_as(user, &block)
495 | Audited.audit_class.as_user(user, &block)
496 | end
497 |
498 | def auditing_enabled
499 | class_auditing_enabled && Audited.auditing_enabled
500 | end
501 |
502 | def auditing_enabled=(val)
503 | Audited.store["#{table_name}_auditing_enabled"] = val
504 | end
505 |
506 | def default_ignored_attributes
507 | [primary_key, inheritance_column] | Audited.ignored_attributes
508 | end
509 |
510 | protected
511 |
512 | def normalize_audited_options
513 | audited_options[:on] = Array.wrap(audited_options[:on])
514 | audited_options[:on] = ([:create, :update, :touch, :destroy] - Audited.ignored_default_callbacks) if audited_options[:on].empty?
515 | audited_options[:only] = Array.wrap(audited_options[:only]).map(&:to_s)
516 | audited_options[:except] = Array.wrap(audited_options[:except]).map(&:to_s)
517 | audited_options[:max_audits] ||= Audited.max_audits
518 | end
519 |
520 | def calculate_non_audited_columns
521 | if audited_options[:only].present?
522 | (column_names | default_ignored_attributes) - audited_options[:only]
523 | elsif audited_options[:except].present?
524 | default_ignored_attributes | audited_options[:except]
525 | else
526 | default_ignored_attributes
527 | end
528 | end
529 |
530 | def class_auditing_enabled
531 | Audited.store.fetch("#{table_name}_auditing_enabled", true)
532 | end
533 | end
534 | end
535 | end
536 |
--------------------------------------------------------------------------------
/spec/audited/auditor_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | # not testing proxy_respond_to? hack / 2 methods / deprecation of `version`
4 | # also, an additional 6 around `after_touch` for Versions before 6.
5 | uncovered = (ActiveRecord::VERSION::MAJOR < 6) ? 15 : 9
6 | SingleCov.covered! uncovered: uncovered
7 |
8 | class ConditionalPrivateCompany < ::ActiveRecord::Base
9 | self.table_name = "companies"
10 |
11 | audited if: :foo?
12 |
13 | private def foo?
14 | true
15 | end
16 | end
17 |
18 | class ConditionalCompany < ::ActiveRecord::Base
19 | self.table_name = "companies"
20 |
21 | audited if: :public?
22 |
23 | def public?
24 | end
25 | end
26 |
27 | class ExclusiveCompany < ::ActiveRecord::Base
28 | self.table_name = "companies"
29 | audited if: proc { false }
30 | end
31 |
32 | class ExclusionaryCompany < ::ActiveRecord::Base
33 | self.table_name = "companies"
34 |
35 | audited unless: :non_profit?
36 |
37 | def non_profit?
38 | end
39 | end
40 |
41 | class ExclusionaryCompany2 < ::ActiveRecord::Base
42 | self.table_name = "companies"
43 | audited unless: proc { |c| c.exclusive? }
44 |
45 | def exclusive?
46 | true
47 | end
48 | end
49 |
50 | class InclusiveCompany < ::ActiveRecord::Base
51 | self.table_name = "companies"
52 | audited if: proc { true }
53 | end
54 |
55 | class InclusiveCompany2 < ::ActiveRecord::Base
56 | self.table_name = "companies"
57 | audited unless: proc { false }
58 | end
59 |
60 | class Secret < ::ActiveRecord::Base
61 | audited
62 | end
63 |
64 | class Secret2 < ::ActiveRecord::Base
65 | audited
66 | self.non_audited_columns = ["delta", "top_secret", "created_at"]
67 | end
68 |
69 | describe Audited::Auditor do
70 | describe "configuration" do
71 | it "should include instance methods" do
72 | expect(Models::ActiveRecord::User.new).to be_a_kind_of(Audited::Auditor::AuditedInstanceMethods)
73 | end
74 |
75 | it "should include class methods" do
76 | expect(Models::ActiveRecord::User).to be_a_kind_of(Audited::Auditor::AuditedClassMethods)
77 | end
78 |
79 | ["created_at", "updated_at", "created_on", "updated_on", "lock_version", "id", "password"].each do |column|
80 | it "should not audit #{column}" do
81 | expect(Models::ActiveRecord::User.non_audited_columns).to include(column)
82 | end
83 | end
84 |
85 | context "should be configurable which conditions are audited" do
86 | subject { ConditionalCompany.new.send(:auditing_enabled) }
87 |
88 | context "when condition method is private" do
89 | subject { ConditionalPrivateCompany.new.send(:auditing_enabled) }
90 |
91 | it { is_expected.to be_truthy }
92 | end
93 |
94 | context "when passing a method name" do
95 | context "when conditions are true" do
96 | before { allow_any_instance_of(ConditionalCompany).to receive(:public?).and_return(true) }
97 | it { is_expected.to be_truthy }
98 | end
99 |
100 | context "when conditions are false" do
101 | before { allow_any_instance_of(ConditionalCompany).to receive(:public?).and_return(false) }
102 | it { is_expected.to be_falsey }
103 | end
104 | end
105 |
106 | context "when passing a Proc" do
107 | context "when conditions are true" do
108 | subject { InclusiveCompany.new.send(:auditing_enabled) }
109 |
110 | it { is_expected.to be_truthy }
111 | end
112 |
113 | context "when conditions are false" do
114 | subject { ExclusiveCompany.new.send(:auditing_enabled) }
115 | it { is_expected.to be_falsey }
116 | end
117 | end
118 | end
119 |
120 | context "should be configurable which conditions aren't audited" do
121 | context "when using a method name" do
122 | subject { ExclusionaryCompany.new.send(:auditing_enabled) }
123 |
124 | context "when conditions are true" do
125 | before { allow_any_instance_of(ExclusionaryCompany).to receive(:non_profit?).and_return(true) }
126 | it { is_expected.to be_falsey }
127 | end
128 |
129 | context "when conditions are false" do
130 | before { allow_any_instance_of(ExclusionaryCompany).to receive(:non_profit?).and_return(false) }
131 | it { is_expected.to be_truthy }
132 | end
133 | end
134 |
135 | context "when using a proc" do
136 | context "when conditions are true" do
137 | subject { ExclusionaryCompany2.new.send(:auditing_enabled) }
138 | it { is_expected.to be_falsey }
139 | end
140 |
141 | context "when conditions are false" do
142 | subject { InclusiveCompany2.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", "updated_at"]
150 |
151 | expect(Secret.non_audited_columns).to include("delta", "top_secret", "created_at")
152 | end
153 |
154 | it "should be configurable which attributes are not audited via non_audited_columns=" do
155 | expect(Secret2.non_audited_columns).to include("delta", "top_secret", "created_at")
156 | end
157 |
158 | it "should not save non-audited columns" do
159 | previous = Models::ActiveRecord::User.non_audited_columns
160 | begin
161 | Models::ActiveRecord::User.non_audited_columns += [:favourite_device]
162 |
163 | expect(create_user.audits.first.audited_changes.keys.any? { |col| ["favourite_device", "created_at", "updated_at", "password"].include?(col) }).to eq(false)
164 | ensure
165 | Models::ActiveRecord::User.non_audited_columns = previous
166 | end
167 | end
168 |
169 | it "should not save other columns than specified in 'only' option" do
170 | user = Models::ActiveRecord::UserOnlyPassword.create
171 | user.instance_eval do
172 | def non_column_attr
173 | @non_column_attr
174 | end
175 |
176 | def non_column_attr=(val)
177 | attribute_will_change!("non_column_attr")
178 | @non_column_attr = val
179 | end
180 | end
181 |
182 | user.password = "password"
183 | user.non_column_attr = "some value"
184 | user.save!
185 | expect(user.audits.last.audited_changes.keys).to eq(%w[password])
186 | end
187 |
188 | it "should save attributes not specified in 'except' option" do
189 | user = Models::ActiveRecord::User.create
190 | user.instance_eval do
191 | def non_column_attr
192 | @non_column_attr
193 | end
194 |
195 | def non_column_attr=(val)
196 | attribute_will_change!("non_column_attr")
197 | @non_column_attr = val
198 | end
199 | end
200 |
201 | user.password = "password"
202 | user.non_column_attr = "some value"
203 | user.save!
204 | expect(user.audits.last.audited_changes.keys).to eq(%w[non_column_attr])
205 | end
206 |
207 | it "should redact columns specified in 'redacted' option" do
208 | redacted = Audited::Auditor::AuditedInstanceMethods::REDACTED
209 | user = Models::ActiveRecord::UserRedactedPassword.create(password: "password")
210 | user.save!
211 | expect(user.audits.last.audited_changes["password"]).to eq(redacted)
212 | user.password = "new_password"
213 | user.save!
214 | expect(user.audits.last.audited_changes["password"]).to eq([redacted, redacted])
215 | end
216 |
217 | it "should redact columns specified in 'redacted' option when there are multiple specified" do
218 | redacted = Audited::Auditor::AuditedInstanceMethods::REDACTED
219 | user =
220 | Models::ActiveRecord::UserMultipleRedactedAttributes.create(
221 | password: "password"
222 | )
223 | user.save!
224 | expect(user.audits.last.audited_changes["password"]).to eq(redacted)
225 | # Saving '[REDACTED]' value for 'ssn' even if value wasn't set explicitly when record was created
226 | expect(user.audits.last.audited_changes["ssn"]).to eq(redacted)
227 |
228 | user.password = "new_password"
229 | user.ssn = 987654321
230 | user.save!
231 | expect(user.audits.last.audited_changes["password"]).to eq([redacted, redacted])
232 | expect(user.audits.last.audited_changes["ssn"]).to eq([redacted, redacted])
233 |
234 | # If we haven't changed any attrs from 'redacted' list, audit should not contain these keys
235 | user.name = "new name"
236 | user.save!
237 | expect(user.audits.last.audited_changes).to have_key("name")
238 | expect(user.audits.last.audited_changes).not_to have_key("password")
239 | expect(user.audits.last.audited_changes).not_to have_key("ssn")
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 | context "when ignored_default_callbacks is set" do
249 | before { Audited.ignored_default_callbacks = [:create] }
250 | after { Audited.ignored_default_callbacks = [] }
251 |
252 | it "should remove create callback" do
253 | class DefaultCallback < ::ActiveRecord::Base
254 | audited
255 | end
256 |
257 | expect(DefaultCallback.audited_options[:on]).to eq([:update, :touch, :destroy])
258 | end
259 |
260 | it "should keep create callback if specified" do
261 | class CallbacksSpecified < ::ActiveRecord::Base
262 | audited on: [:create, :update, :destroy]
263 | end
264 |
265 | expect(CallbacksSpecified.audited_options[:on]).to eq([:create, :update, :destroy])
266 | end
267 | end
268 |
269 | if ::ActiveRecord::VERSION::MAJOR >= 7
270 | it "should filter encrypted attributes" do
271 | user = Models::ActiveRecord::UserWithEncryptedPassword.create(password: "password")
272 | user.save
273 | expect(user.audits.last.audited_changes["password"]).to eq("[FILTERED]")
274 | end
275 | end
276 |
277 | if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
278 | describe "'json' and 'jsonb' audited_changes column type" do
279 | let(:migrations_path) { SPEC_ROOT.join("support/active_record/postgres") }
280 |
281 | after do
282 | run_migrations(:down, migrations_path)
283 | end
284 |
285 | it "should work if column type is 'json'" do
286 | run_migrations(:up, migrations_path, 1)
287 | Audited::Audit.reset_column_information
288 | expect(Audited::Audit.columns_hash["audited_changes"].sql_type).to eq("json")
289 |
290 | user = Models::ActiveRecord::User.create
291 | user.name = "new name"
292 | user.save!
293 | expect(user.audits.last.audited_changes).to eq({"name" => [nil, "new name"]})
294 | end
295 |
296 | it "should work if column type is 'jsonb'" do
297 | run_migrations(:up, migrations_path, 2)
298 | Audited::Audit.reset_column_information
299 | expect(Audited::Audit.columns_hash["audited_changes"].sql_type).to eq("jsonb")
300 |
301 | user = Models::ActiveRecord::User.create
302 | user.name = "new name"
303 | user.save!
304 | expect(user.audits.last.audited_changes).to eq({"name" => [nil, "new name"]})
305 | end
306 | end
307 | end
308 | end
309 |
310 | describe :new do
311 | it "should allow mass assignment of all unprotected attributes" do
312 | yesterday = 1.day.ago
313 |
314 | u = Models::ActiveRecord::NoAttributeProtectionUser.new(name: "name",
315 | username: "username",
316 | password: "password",
317 | activated: true,
318 | suspended_at: yesterday,
319 | logins: 2)
320 |
321 | expect(u.name).to eq("name")
322 | expect(u.username).to eq("username")
323 | expect(u.password).to eq("password")
324 | expect(u.activated).to eq(true)
325 | expect(u.suspended_at.to_i).to eq(yesterday.to_i)
326 | expect(u.logins).to eq(2)
327 | end
328 | end
329 |
330 | describe "on create" do
331 | let(:user) { create_user status: :reliable, audit_comment: "Create" }
332 |
333 | it "should change the audit count" do
334 | expect {
335 | user
336 | }.to change(Audited::Audit, :count).by(1)
337 | end
338 |
339 | it "should create associated audit" do
340 | expect(user.audits.count).to eq(1)
341 | end
342 |
343 | it "should set the action to create" do
344 | expect(user.audits.first.action).to eq("create")
345 | expect(Audited::Audit.creates.order(:id).last).to eq(user.audits.first)
346 | expect(user.audits.creates.count).to eq(1)
347 | expect(user.audits.updates.count).to eq(0)
348 | expect(user.audits.destroys.count).to eq(0)
349 | end
350 |
351 | it "should store all the audited attributes" do
352 | expect(user.audits.first.audited_changes).to eq(user.audited_attributes)
353 | end
354 |
355 | it "should store enum value" do
356 | expect(user.audits.first.audited_changes["status"]).to eq(1)
357 | end
358 |
359 | context "when store_synthesized_enums is set to true" do
360 | before { Audited.store_synthesized_enums = true }
361 | after { Audited.store_synthesized_enums = false }
362 |
363 | it "should store enum value as Rails synthesized value" do
364 | expect(user.audits.first.audited_changes["status"]).to eq("reliable")
365 | end
366 | end
367 |
368 | it "should store comment" do
369 | expect(user.audits.first.comment).to eq("Create")
370 | end
371 |
372 | it "should not audit an attribute which is excepted if specified on create or destroy" do
373 | on_create_destroy_except_name = Models::ActiveRecord::OnCreateDestroyExceptName.create(name: "Bart")
374 | expect(on_create_destroy_except_name.audits.first.audited_changes.keys.any? { |col| ["name"].include? col }).to eq(false)
375 | end
376 |
377 | it "should not save an audit if only specified on update/destroy" do
378 | expect {
379 | Models::ActiveRecord::OnUpdateDestroy.create!(name: "Bart")
380 | }.to_not change(Audited::Audit, :count)
381 | end
382 |
383 | it "should save readonly columns" do
384 | expect {
385 | Models::ActiveRecord::UserWithReadOnlyAttrs.create!(name: "Bart")
386 | }.to change(Audited::Audit, :count)
387 | end
388 | end
389 |
390 | describe "on update" do
391 | before do
392 | @user = create_user(name: "Brandon", status: :active, audit_comment: "Update")
393 | end
394 |
395 | it "should save an audit" do
396 | expect {
397 | @user.update_attribute(:name, "Someone")
398 | }.to change(Audited::Audit, :count).by(1)
399 | expect {
400 | @user.update_attribute(:name, "Someone else")
401 | }.to change(Audited::Audit, :count).by(1)
402 | end
403 |
404 | it "should set the action to 'update'" do
405 | @user.update! name: "Changed"
406 | expect(@user.audits.last.action).to eq("update")
407 | expect(Audited::Audit.updates.order(:id).last).to eq(@user.audits.last)
408 | expect(@user.audits.updates.last).to eq(@user.audits.last)
409 | end
410 |
411 | it "should store the changed attributes" do
412 | @user.update! name: "Changed"
413 | expect(@user.audits.last.audited_changes).to eq({"name" => ["Brandon", "Changed"]})
414 | end
415 |
416 | it "should store changed enum values" do
417 | @user.update! status: 1
418 | expect(@user.audits.last.audited_changes["status"]).to eq([0, 1])
419 | end
420 |
421 | it "should store audit comment" do
422 | expect(@user.audits.last.comment).to eq("Update")
423 | end
424 |
425 | it "should not save an audit if only specified on create/destroy" do
426 | on_create_destroy = Models::ActiveRecord::OnCreateDestroy.create(name: "Bart")
427 | expect {
428 | on_create_destroy.update! name: "Changed"
429 | }.to_not change(Audited::Audit, :count)
430 | end
431 |
432 | it "should not save an audit if the value doesn't change after type casting" do
433 | @user.update! logins: 0, activated: true
434 | expect { @user.update_attribute :logins, "0" }.to_not change(Audited::Audit, :count)
435 | expect { @user.update_attribute :activated, 1 }.to_not change(Audited::Audit, :count)
436 | expect { @user.update_attribute :activated, "1" }.to_not change(Audited::Audit, :count)
437 | end
438 |
439 | context "with readonly attributes" do
440 | before do
441 | @user = create_user_with_readonly_attrs(status: "active")
442 | end
443 |
444 | it "should not save readonly columns" do
445 | expect { @user.update! status: "banned" }.to_not change(Audited::Audit, :count)
446 | end
447 | end
448 |
449 | describe "with no dirty changes" do
450 | it "does not create an audit if the record is not changed" do
451 | expect {
452 | @user.save!
453 | }.to_not change(Audited::Audit, :count)
454 | end
455 |
456 | it "creates an audit when an audit comment is present" do
457 | expect {
458 | @user.audit_comment = "Comment"
459 | @user.save!
460 | }.to change(Audited::Audit, :count)
461 | end
462 | end
463 | end
464 |
465 | if ::ActiveRecord::VERSION::MAJOR >= 6
466 | describe "on touch" do
467 | before do
468 | @user = create_user(name: "Brandon", status: :active)
469 | end
470 |
471 | it "should save an audit" do
472 | expect { @user.touch(:suspended_at) }.to change(Audited::Audit, :count).by(1)
473 | end
474 |
475 | it "should set the action to 'update'" do
476 | @user.touch(:suspended_at)
477 | expect(@user.audits.last.action).to eq("update")
478 | expect(Audited::Audit.updates.order(:id).last).to eq(@user.audits.last)
479 | expect(@user.audits.updates.last).to eq(@user.audits.last)
480 | end
481 |
482 | it "should store the changed attributes" do
483 | @user.touch(:suspended_at)
484 | expect(@user.audits.last.audited_changes["suspended_at"][0]).to be_nil
485 | expect(Time.parse(@user.audits.last.audited_changes["suspended_at"][1].to_s)).to be_within(2.seconds).of(Time.current)
486 | end
487 |
488 | it "should store audit comment" do
489 | @user.audit_comment = "Here exists a touch comment"
490 | @user.touch(:suspended_at)
491 | expect(@user.audits.last.action).to eq("update")
492 | expect(@user.audits.last.comment).to eq("Here exists a touch comment")
493 | end
494 |
495 | it "should not save an audit if only specified on create/destroy" do
496 | on_create_destroy = Models::ActiveRecord::OnCreateDestroyUser.create(name: "Bart")
497 | expect {
498 | on_create_destroy.touch(:suspended_at)
499 | }.to_not change(Audited::Audit, :count)
500 | end
501 |
502 | it "should store an audit if touch is the only audit" do
503 | on_touch = Models::ActiveRecord::OnTouchOnly.create(name: "Bart")
504 | expect {
505 | on_touch.update(name: "NotBart")
506 | }.to_not change(Audited::Audit, :count)
507 | expect {
508 | on_touch.touch(:suspended_at)
509 | }.to change(on_touch.audits, :count).from(0).to(1)
510 |
511 | @user.audits.destroy_all
512 | expect(@user.audits).to be_empty
513 | expect {
514 | @user.touch(:suspended_at)
515 | }.to change(@user.audits, :count).from(0).to(1)
516 | end
517 |
518 | context "don't double audit" do
519 | let(:user) { Models::ActiveRecord::Owner.create(name: "OwnerUser", suspended_at: 1.month.ago, companies_attributes: [{name: "OwnedCompany"}]) }
520 | let(:company) { user.companies.first }
521 |
522 | it "should only create 1 (create) audit for object" do
523 | expect(user.audits.count).to eq(1)
524 | expect(user.audits.first.action).to eq("create")
525 | end
526 |
527 | it "should only create 1 (create) audit for nested resource" do
528 | expect(company.audits.count).to eq(1)
529 | expect(company.audits.first.action).to eq("create")
530 | end
531 |
532 | context "after creating" do
533 | it "updating / touching nested resource shouldn't save touch audit on parent object" do
534 | expect { company.touch(:type) }.not_to change(user.audits, :count)
535 | expect { company.update(type: "test") }.not_to change(user.audits, :count)
536 | end
537 |
538 | it "updating / touching parent object shouldn't save previous data" do
539 | expect { user.touch(:suspended_at) }.to change(user.audits, :count).from(1).to(2)
540 | expect(user.audits.last.action).to eq("update")
541 | expect(user.audits.last.audited_changes.keys).to eq(%w[suspended_at])
542 | end
543 |
544 | it "updating nested resource through parent while changing an enum on parent shouldn't double audit" do
545 | user.status = :reliable
546 | user.companies_attributes = [{name: "test"}]
547 | expect { user.save }.to change(user.audits, :count).from(1).to(2)
548 | expect(user.audits.last.action).to eq("update")
549 | expect(user.audits.last.audited_changes.keys).to eq(%w[status])
550 | end
551 | end
552 |
553 | context "after updating" do
554 | it "changing nested resource shouldn't audit owner" do
555 | expect { user.update(username: "test") }.to change(user.audits, :count).from(1).to(2)
556 | expect { company.update(type: "test") }.not_to change(user.audits, :count)
557 |
558 | expect { user.touch(:suspended_at) }.to change(user.audits, :count).from(2).to(3)
559 | expect { company.update(type: "another_test") }.not_to change(user.audits, :count)
560 | end
561 | end
562 | end
563 | end
564 | end
565 |
566 | describe "on destroy" do
567 | before do
568 | @user = create_user(status: :active)
569 | end
570 |
571 | it "should save an audit" do
572 | expect {
573 | @user.destroy
574 | }.to change(Audited::Audit, :count)
575 |
576 | expect(@user.audits.size).to eq(2)
577 | end
578 |
579 | it "should set the action to 'destroy'" do
580 | @user.destroy
581 |
582 | expect(@user.audits.last.action).to eq("destroy")
583 | expect(Audited::Audit.destroys.order(:id).last).to eq(@user.audits.last)
584 | expect(@user.audits.destroys.last).to eq(@user.audits.last)
585 | end
586 |
587 | it "should store all of the audited attributes" do
588 | @user.destroy
589 |
590 | expect(@user.audits.last.audited_changes).to eq(@user.audited_attributes)
591 | end
592 |
593 | it "should store enum value" do
594 | @user.destroy
595 | expect(@user.audits.last.audited_changes["status"]).to eq(0)
596 | end
597 |
598 | it "should be able to reconstruct a destroyed record without history" do
599 | @user.audits.delete_all
600 | @user.destroy
601 |
602 | revision = @user.audits.first.revision
603 | expect(revision.name).to eq(@user.name)
604 | end
605 |
606 | it "should not save an audit if only specified on create/update" do
607 | on_create_update = Models::ActiveRecord::OnCreateUpdate.create!(name: "Bart")
608 |
609 | expect {
610 | on_create_update.destroy
611 | }.to_not change(Audited::Audit, :count)
612 | end
613 |
614 | it "should audit dependent destructions" do
615 | owner = Models::ActiveRecord::Owner.create!
616 | company = owner.companies.create!
617 |
618 | expect {
619 | owner.destroy
620 | }.to change(Audited::Audit, :count)
621 |
622 | expect(company.audits.map { |a| a.action }).to eq(["create", "destroy"])
623 | end
624 | end
625 |
626 | describe "on destroy with unsaved object" do
627 | let(:user) { Models::ActiveRecord::User.new }
628 |
629 | it "should not audit on 'destroy'" do
630 | expect {
631 | user.destroy
632 | }.to_not raise_error
633 |
634 | expect(user.audits).to be_empty
635 | end
636 | end
637 |
638 | describe "associated with" do
639 | let(:owner) { Models::ActiveRecord::Owner.create(name: "Models::ActiveRecord::Owner") }
640 | let(:owned_company) { Models::ActiveRecord::OwnedCompany.create!(name: "The auditors", owner: owner) }
641 |
642 | it "should record the associated object on create" do
643 | expect(owned_company.audits.first.associated).to eq(owner)
644 | end
645 |
646 | it "should store the associated object on update" do
647 | owned_company.update_attribute(:name, "The Auditors")
648 | expect(owned_company.audits.last.associated).to eq(owner)
649 | end
650 |
651 | it "should store the associated object on destroy" do
652 | owned_company.destroy
653 | expect(owned_company.audits.last.associated).to eq(owner)
654 | end
655 | end
656 |
657 | describe "has associated audits" do
658 | let!(:owner) { Models::ActiveRecord::Owner.create!(name: "Models::ActiveRecord::Owner") }
659 | let!(:owned_company) { Models::ActiveRecord::OwnedCompany.create!(name: "The auditors", owner: owner) }
660 |
661 | it "should list the associated audits" do
662 | expect(owner.associated_audits.length).to eq(1)
663 | expect(owner.associated_audits.first.auditable).to eq(owned_company)
664 | end
665 | end
666 |
667 | describe "max_audits" do
668 | it "should respect global setting" do
669 | stub_global_max_audits(10) do
670 | expect(Models::ActiveRecord::User.audited_options[:max_audits]).to eq(10)
671 | end
672 | end
673 |
674 | it "should respect per model setting" do
675 | stub_global_max_audits(10) do
676 | expect(Models::ActiveRecord::MaxAuditsUser.audited_options[:max_audits]).to eq(5)
677 | end
678 | end
679 |
680 | it "should delete old audits when keeped amount exceeded" do
681 | stub_global_max_audits(2) do
682 | user = create_versions(2)
683 | user.update(name: "John")
684 | expect(user.audits.pluck(:version)).to eq([2, 3])
685 | end
686 | end
687 |
688 | it "should not delete old audits when keeped amount not exceeded" do
689 | stub_global_max_audits(3) do
690 | user = create_versions(2)
691 | user.update(name: "John")
692 | expect(user.audits.pluck(:version)).to eq([1, 2, 3])
693 | end
694 | end
695 |
696 | it "should delete old extra audits after introducing limit" do
697 | stub_global_max_audits(nil) do
698 | user = Models::ActiveRecord::User.create!(name: "Brandon", username: "brandon")
699 | user.update!(name: "Foobar")
700 | user.update!(name: "Awesome", username: "keepers")
701 | user.update!(activated: true)
702 |
703 | Audited.max_audits = 3
704 | Models::ActiveRecord::User.send(:normalize_audited_options)
705 | user.update!(favourite_device: "Android Phone")
706 | audits = user.audits
707 |
708 | expect(audits.count).to eq(3)
709 | expect(audits[0].audited_changes).to include({"name" => ["Foobar", "Awesome"], "username" => ["brandon", "keepers"]})
710 | expect(audits[1].audited_changes).to eq({"activated" => [nil, true]})
711 | expect(audits[2].audited_changes).to eq({"favourite_device" => [nil, "Android Phone"]})
712 | end
713 | end
714 |
715 | it "should add comment line for combined audit" do
716 | stub_global_max_audits(2) do
717 | user = Models::ActiveRecord::User.create!(name: "Foobar 1")
718 | user.update(name: "Foobar 2", audit_comment: "First audit comment")
719 | user.update(name: "Foobar 3", audit_comment: "Second audit comment")
720 | expect(user.audits.first.comment).to match(/First audit comment.+is the result of multiple/m)
721 | end
722 | end
723 |
724 | def stub_global_max_audits(max_audits)
725 | previous_max_audits = Audited.max_audits
726 | previous_user_audited_options = Models::ActiveRecord::User.audited_options.dup
727 | begin
728 | Audited.max_audits = max_audits
729 | Models::ActiveRecord::User.send(:normalize_audited_options) # reloads audited_options
730 | yield
731 | ensure
732 | Audited.max_audits = previous_max_audits
733 | Models::ActiveRecord::User.audited_options = previous_user_audited_options
734 | end
735 | end
736 | end
737 |
738 | describe "revisions" do
739 | let(:user) { create_versions }
740 |
741 | it "should return an Array of Users" do
742 | expect(user.revisions).to be_a_kind_of(Array)
743 | user.revisions.each { |version| expect(version).to be_a_kind_of Models::ActiveRecord::User }
744 | end
745 |
746 | it "should have one revision for a new record" do
747 | expect(create_user.revisions.size).to eq(1)
748 | end
749 |
750 | it "should have one revision for each audit" do
751 | expect(user.audits.size).to eql(user.revisions.size)
752 | end
753 |
754 | it "should set the attributes for each revision" do
755 | u = Models::ActiveRecord::User.create(name: "Brandon", username: "brandon")
756 | u.update! name: "Foobar"
757 | u.update! name: "Awesome", username: "keepers"
758 |
759 | expect(u.revisions.size).to eql(3)
760 |
761 | expect(u.revisions[0].name).to eql("Brandon")
762 | expect(u.revisions[0].username).to eql("brandon")
763 |
764 | expect(u.revisions[1].name).to eql("Foobar")
765 | expect(u.revisions[1].username).to eql("brandon")
766 |
767 | expect(u.revisions[2].name).to eql("Awesome")
768 | expect(u.revisions[2].username).to eql("keepers")
769 | end
770 |
771 | it "access to only recent revisions" do
772 | u = Models::ActiveRecord::User.create(name: "Brandon", username: "brandon")
773 | u.update! name: "Foobar"
774 | u.update! name: "Awesome", username: "keepers"
775 |
776 | expect(u.revisions(2).size).to eq(2)
777 |
778 | expect(u.revisions(2)[0].name).to eq("Foobar")
779 | expect(u.revisions(2)[0].username).to eq("brandon")
780 |
781 | expect(u.revisions(2)[1].name).to eq("Awesome")
782 | expect(u.revisions(2)[1].username).to eq("keepers")
783 | end
784 |
785 | it "should be empty if no audits exist" do
786 | user.audits.delete_all
787 | expect(user.revisions).to be_empty
788 | end
789 |
790 | it "should ignore attributes that have been deleted" do
791 | user.audits.last.update! audited_changes: {old_attribute: "old value"}
792 | expect { user.revisions }.to_not raise_error
793 | end
794 | end
795 |
796 | describe "revisions" do
797 | let(:user) { create_versions(5) }
798 |
799 | it "should maintain identity" do
800 | expect(user.revision(1)).to eq(user)
801 | end
802 |
803 | it "should find the given revision" do
804 | revision = user.revision(3)
805 | expect(revision).to be_a_kind_of(Models::ActiveRecord::User)
806 | expect(revision.audit_version).to eq(3)
807 | expect(revision.name).to eq("Foobar 3")
808 | end
809 |
810 | it "should find the previous revision with :previous" do
811 | revision = user.revision(:previous)
812 | expect(revision.audit_version).to eq(4)
813 | # expect(revision).to eq(user.revision(4))
814 | expect(revision.attributes).to eq(user.revision(4).attributes)
815 | end
816 |
817 | it "should be able to get the previous revision repeatedly" do
818 | previous = user.revision(:previous)
819 | expect(previous.audit_version).to eq(4)
820 | expect(previous.revision(:previous).audit_version).to eq(3)
821 | end
822 |
823 | it "should be able to set protected attributes" do
824 | u = Models::ActiveRecord::User.create(name: "Brandon")
825 | u.update_attribute :logins, 1
826 | u.update_attribute :logins, 2
827 |
828 | expect(u.revision(3).logins).to eq(2)
829 | expect(u.revision(2).logins).to eq(1)
830 | expect(u.revision(1).logins).to eq(0)
831 | end
832 |
833 | it "should set attributes directly" do
834 | u = Models::ActiveRecord::User.create(name: "")
835 | expect(u.revision(1).name).to eq("<Joe>")
836 | end
837 |
838 | it "should set the attributes for each revision" do
839 | u = Models::ActiveRecord::User.create(name: "Brandon", username: "brandon")
840 | u.update! name: "Foobar"
841 | u.update! name: "Awesome", username: "keepers"
842 |
843 | expect(u.revision(3).name).to eq("Awesome")
844 | expect(u.revision(3).username).to eq("keepers")
845 |
846 | expect(u.revision(2).name).to eq("Foobar")
847 | expect(u.revision(2).username).to eq("brandon")
848 |
849 | expect(u.revision(1).name).to eq("Brandon")
850 | expect(u.revision(1).username).to eq("brandon")
851 | end
852 |
853 | it "should correctly restore revision with enum" do
854 | u = Models::ActiveRecord::User.create(status: :active)
855 | u.update_attribute(:status, :reliable)
856 | u.update_attribute(:status, :banned)
857 |
858 | expect(u.revision(3)).to be_banned
859 | expect(u.revision(2)).to be_reliable
860 | expect(u.revision(1)).to be_active
861 | end
862 |
863 | it "should be able to get time for first revision" do
864 | suspended_at = Time.zone.now
865 | u = Models::ActiveRecord::User.create(suspended_at: suspended_at)
866 | expect(u.revision(1).suspended_at.to_s).to eq(suspended_at.to_s)
867 | end
868 |
869 | it "should not raise an error when no previous audits exist" do
870 | user.audits.destroy_all
871 | expect { user.revision(:previous) }.to_not raise_error
872 | end
873 |
874 | it "should mark revision's attributes as changed" do
875 | expect(user.revision(1).name_changed?).to eq(true)
876 | end
877 |
878 | it "should record new audit when saving revision" do
879 | expect {
880 | user.revision(1).save!
881 | }.to change(user.audits, :count).by(1)
882 | end
883 |
884 | it "should re-insert destroyed records" do
885 | user.destroy
886 | expect {
887 | user.revision(1).save!
888 | }.to change(Models::ActiveRecord::User, :count).by(1)
889 | end
890 |
891 | it "should return nil for values greater than the number of revisions" do
892 | expect(user.revision(user.revisions.count + 1)).to be_nil
893 | end
894 |
895 | it "should work with array attributes" do
896 | u = Models::ActiveRecord::User.create!(phone_numbers: ["+1 800-444-4444"])
897 | u.update!(phone_numbers: ["+1 804-222-1111", "+1 317 222-2222"])
898 |
899 | expect(u.revision(0).phone_numbers).to eq(["+1 804-222-1111", "+1 317 222-2222"])
900 | expect(u.revision(1).phone_numbers).to eq(["+1 800-444-4444"])
901 | end
902 | end
903 |
904 | describe "revision_at" do
905 | let(:user) { create_user }
906 |
907 | it "should find the latest revision before the given time" do
908 | audit = user.audits.first
909 | audit.created_at = 1.hour.ago
910 | audit.save!
911 | user.update! name: "updated"
912 | expect(user.revision_at(2.minutes.ago).audit_version).to eq(1)
913 | end
914 |
915 | it "should be nil if given a time before audits" do
916 | expect(user.revision_at(1.week.ago)).to be_nil
917 | end
918 | end
919 |
920 | describe "own_and_associated_audits" do
921 | it "should return audits for self and associated audits" do
922 | owner = Models::ActiveRecord::Owner.create!
923 | company = owner.companies.create!
924 | company.update!(name: "Collective Idea")
925 |
926 | other_owner = Models::ActiveRecord::Owner.create!
927 | other_owner.companies.create!
928 |
929 | expect(owner.own_and_associated_audits).to match_array(owner.audits + company.audits)
930 | end
931 |
932 | it "should return audits for STI classes" do
933 | # Where parent is STI
934 | sti_company = Models::ActiveRecord::Company::STICompany.create!
935 | sti_company.update!(name: "Collective Idea")
936 | expect(sti_company.own_and_associated_audits).to match_array(sti_company.audits)
937 |
938 | # Where associated is STI
939 | owner = Models::ActiveRecord::Owner.create!
940 | company = owner.companies.create! type: "Models::ActiveRecord::OwnedCompany::STICompany"
941 | company.update!(name: "Collective Idea")
942 | expect(owner.own_and_associated_audits).to match_array(owner.audits + company.audits)
943 | end
944 |
945 | it "should order audits by creation time" do
946 | owner = Models::ActiveRecord::Owner.create!
947 | first_audit = owner.audits.first
948 | first_audit.update_column(:created_at, 1.year.ago)
949 |
950 | company = owner.companies.create!
951 | second_audit = company.audits.first
952 | second_audit.update_column(:created_at, 1.month.ago)
953 |
954 | company.update!(name: "Collective Idea")
955 | third_audit = company.audits.last
956 | expect(owner.own_and_associated_audits.to_a).to eq([third_audit, second_audit, first_audit])
957 | end
958 | end
959 |
960 | describe "without auditing" do
961 | it "should not save an audit when calling #save_without_auditing" do
962 | expect {
963 | u = Models::ActiveRecord::User.new(name: "Brandon")
964 | expect(u.save_without_auditing).to eq(true)
965 | }.to_not change(Audited::Audit, :count)
966 | end
967 |
968 | it "should not save an audit inside of the #without_auditing block" do
969 | expect {
970 | Models::ActiveRecord::User.without_auditing { Models::ActiveRecord::User.create!(name: "Brandon") }
971 | }.to_not change(Audited::Audit, :count)
972 | end
973 |
974 | context "when global audits are disabled" do
975 | it "should re-enable class audits after #without_auditing block" do
976 | Audited.auditing_enabled = false
977 | Models::ActiveRecord::User.without_auditing {}
978 | Audited.auditing_enabled = true
979 | expect(Models::ActiveRecord::User.auditing_enabled).to eql(true)
980 | end
981 | end
982 |
983 | it "should reset auditing status even it raises an exception" do
984 | begin
985 | Models::ActiveRecord::User.without_auditing { raise }
986 | rescue
987 | nil
988 | end
989 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
990 | end
991 |
992 | it "should be thread safe using a #without_auditing block" do
993 | skip if Models::ActiveRecord::User.connection.class.name.include?("SQLite")
994 |
995 | t1 = Thread.new do
996 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
997 | Models::ActiveRecord::User.without_auditing do
998 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
999 | Models::ActiveRecord::User.create!(name: "Bart")
1000 | sleep 1
1001 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
1002 | end
1003 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
1004 | end
1005 |
1006 | t2 = Thread.new do
1007 | sleep 0.5
1008 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
1009 | Models::ActiveRecord::User.create!(name: "Lisa")
1010 | end
1011 | t1.join
1012 | t2.join
1013 |
1014 | expect(Models::ActiveRecord::User.find_by_name("Bart").audits.count).to eq(0)
1015 | expect(Models::ActiveRecord::User.find_by_name("Lisa").audits.count).to eq(1)
1016 | end
1017 |
1018 | it "should not save an audit when auditing is globally disabled" do
1019 | expect(Audited.auditing_enabled).to eq(true)
1020 | Audited.auditing_enabled = false
1021 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
1022 |
1023 | user = create_user
1024 | expect(user.audits.count).to eq(0)
1025 |
1026 | Audited.auditing_enabled = true
1027 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
1028 |
1029 | user.update!(name: "Test")
1030 | expect(user.audits.count).to eq(1)
1031 | Models::ActiveRecord::User.enable_auditing
1032 | end
1033 | end
1034 |
1035 | describe "with auditing" do
1036 | it "should save an audit when calling #save_with_auditing" do
1037 | expect {
1038 | u = Models::ActiveRecord::User.new(name: "Brandon")
1039 | Models::ActiveRecord::User.auditing_enabled = false
1040 | expect(u.save_with_auditing).to eq(true)
1041 | Models::ActiveRecord::User.auditing_enabled = true
1042 | }.to change(Audited::Audit, :count).by(1)
1043 | end
1044 |
1045 | it "should save an audit inside of the #with_auditing block" do
1046 | expect {
1047 | Models::ActiveRecord::User.auditing_enabled = false
1048 | Models::ActiveRecord::User.with_auditing { Models::ActiveRecord::User.create!(name: "Brandon") }
1049 | Models::ActiveRecord::User.auditing_enabled = true
1050 | }.to change(Audited::Audit, :count).by(1)
1051 | end
1052 |
1053 | context "when global audits are disabled" do
1054 | it "should re-enable class audits after #with_auditing block" do
1055 | Audited.auditing_enabled = false
1056 | Models::ActiveRecord::User.with_auditing {}
1057 | Audited.auditing_enabled = true
1058 | expect(Models::ActiveRecord::User.auditing_enabled).to eql(true)
1059 | end
1060 | end
1061 |
1062 | it "should reset auditing status even it raises an exception" do
1063 | Models::ActiveRecord::User.disable_auditing
1064 | begin
1065 | Models::ActiveRecord::User.with_auditing { raise }
1066 | rescue
1067 | nil
1068 | end
1069 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
1070 | Models::ActiveRecord::User.enable_auditing
1071 | end
1072 |
1073 | it "should be thread safe using a #with_auditing block" do
1074 | skip if Models::ActiveRecord::User.connection.class.name.include?("SQLite")
1075 |
1076 | t1 = Thread.new do
1077 | Models::ActiveRecord::User.disable_auditing
1078 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
1079 | Models::ActiveRecord::User.with_auditing do
1080 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
1081 |
1082 | Models::ActiveRecord::User.create!(name: "Shaggy")
1083 | sleep 1
1084 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
1085 | end
1086 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
1087 | Models::ActiveRecord::User.enable_auditing
1088 | end
1089 |
1090 | t2 = Thread.new do
1091 | sleep 0.5
1092 | Models::ActiveRecord::User.disable_auditing
1093 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
1094 | Models::ActiveRecord::User.create!(name: "Scooby")
1095 | Models::ActiveRecord::User.enable_auditing
1096 | end
1097 | t1.join
1098 | t2.join
1099 |
1100 | Models::ActiveRecord::User.enable_auditing
1101 | expect(Models::ActiveRecord::User.find_by_name("Shaggy").audits.count).to eq(1)
1102 | expect(Models::ActiveRecord::User.find_by_name("Scooby").audits.count).to eq(0)
1103 | end
1104 | end
1105 |
1106 | describe "comment required" do
1107 | describe "on create" do
1108 | it "should not validate when audit_comment is not supplied when initialized" do
1109 | expect(Models::ActiveRecord::CommentRequiredUser.new(name: "Foo")).not_to be_valid
1110 | end
1111 |
1112 | it "should not validate when audit_comment is not supplied trying to create" do
1113 | expect(Models::ActiveRecord::CommentRequiredUser.create(name: "Foo")).not_to be_valid
1114 | end
1115 |
1116 | it "should validate when audit_comment is supplied" do
1117 | expect(Models::ActiveRecord::CommentRequiredUser.create(name: "Foo", audit_comment: "Create")).to be_valid
1118 | end
1119 |
1120 | it "should validate when audit_comment is not supplied, and creating is not being audited" do
1121 | expect(Models::ActiveRecord::OnUpdateCommentRequiredUser.create(name: "Foo")).to be_valid
1122 | expect(Models::ActiveRecord::OnDestroyCommentRequiredUser.create(name: "Foo")).to be_valid
1123 | end
1124 |
1125 | it "should validate when audit_comment is not supplied, and auditing is disabled" do
1126 | Models::ActiveRecord::CommentRequiredUser.disable_auditing
1127 | expect(Models::ActiveRecord::CommentRequiredUser.create(name: "Foo")).to be_valid
1128 | Models::ActiveRecord::CommentRequiredUser.enable_auditing
1129 | end
1130 |
1131 | it "should validate when audit_comment is not supplied, and only excluded attributes changed" do
1132 | expect(Models::ActiveRecord::CommentRequiredUser.new(password: "Foo")).to be_valid
1133 | end
1134 | end
1135 |
1136 | describe "on update" do
1137 | let(:user) { Models::ActiveRecord::CommentRequiredUser.create!(audit_comment: "Create") }
1138 | let(:on_create_user) { Models::ActiveRecord::OnDestroyCommentRequiredUser.create }
1139 | let(:on_destroy_user) { Models::ActiveRecord::OnDestroyCommentRequiredUser.create }
1140 |
1141 | it "should not validate when audit_comment is not supplied" do
1142 | expect(user.update(name: "Test")).to eq(false)
1143 | end
1144 |
1145 | it "should validate when audit_comment is not supplied, and updating is not being audited" do
1146 | expect(on_create_user.update(name: "Test")).to eq(true)
1147 | expect(on_destroy_user.update(name: "Test")).to eq(true)
1148 | end
1149 |
1150 | it "should validate when audit_comment is supplied" do
1151 | expect(user.update(name: "Test", audit_comment: "Update")).to eq(true)
1152 | end
1153 |
1154 | it "should validate when audit_comment is not supplied, and auditing is disabled" do
1155 | Models::ActiveRecord::CommentRequiredUser.disable_auditing
1156 | expect(user.update(name: "Test")).to eq(true)
1157 | Models::ActiveRecord::CommentRequiredUser.enable_auditing
1158 | end
1159 |
1160 | it "should validate when audit_comment is not supplied, and only excluded attributes changed" do
1161 | expect(user.update(password: "Test")).to eq(true)
1162 | end
1163 | end
1164 |
1165 | describe "on destroy" do
1166 | let(:user) { Models::ActiveRecord::CommentRequiredUser.create!(audit_comment: "Create") }
1167 | let(:on_create_user) { Models::ActiveRecord::OnCreateCommentRequiredUser.create!(audit_comment: "Create") }
1168 | let(:on_update_user) { Models::ActiveRecord::OnUpdateCommentRequiredUser.create }
1169 |
1170 | it "should not validate when audit_comment is not supplied" do
1171 | expect(user.destroy).to eq(false)
1172 | end
1173 |
1174 | it "should validate when audit_comment is supplied" do
1175 | user.audit_comment = "Destroy"
1176 | expect(user.destroy).to eq(user)
1177 | end
1178 |
1179 | it "should validate when audit_comment is not supplied, and destroying is not being audited" do
1180 | expect(on_create_user.destroy).to eq(on_create_user)
1181 | expect(on_update_user.destroy).to eq(on_update_user)
1182 | end
1183 |
1184 | it "should validate when audit_comment is not supplied, and auditing is disabled" do
1185 | Models::ActiveRecord::CommentRequiredUser.disable_auditing
1186 | expect(user.destroy).to eq(user)
1187 | Models::ActiveRecord::CommentRequiredUser.enable_auditing
1188 | end
1189 | end
1190 | end
1191 |
1192 | describe "no update with comment only" do
1193 | let(:user) { Models::ActiveRecord::NoUpdateWithCommentOnlyUser.create }
1194 |
1195 | it "does not create an audit when only an audit_comment is present" do
1196 | user.audit_comment = "Comment"
1197 | expect { user.save! }.to_not change(Audited::Audit, :count)
1198 | end
1199 | end
1200 |
1201 | describe "attr_protected and attr_accessible" do
1202 | it "should not raise error when attr_accessible is set and protected is false" do
1203 | expect {
1204 | Models::ActiveRecord::AccessibleAfterDeclarationUser.new(name: "No fail!")
1205 | }.to_not raise_error
1206 | end
1207 |
1208 | it "should not rause an error when attr_accessible is declared before audited" do
1209 | expect {
1210 | Models::ActiveRecord::AccessibleAfterDeclarationUser.new(name: "No fail!")
1211 | }.to_not raise_error
1212 | end
1213 | end
1214 |
1215 | describe "audit_as" do
1216 | let(:user) { Models::ActiveRecord::User.create name: "Testing" }
1217 |
1218 | it "should record user objects" do
1219 | Models::ActiveRecord::Company.audit_as(user) do
1220 | company = Models::ActiveRecord::Company.create name: "The auditors"
1221 | company.update! name: "The Auditors"
1222 |
1223 | company.audits.each do |audit|
1224 | expect(audit.user).to eq(user)
1225 | end
1226 | end
1227 | end
1228 |
1229 | it "should record usernames" do
1230 | Models::ActiveRecord::Company.audit_as(user.name) do
1231 | company = Models::ActiveRecord::Company.create name: "The auditors"
1232 | company.update! name: "The Auditors"
1233 |
1234 | company.audits.each do |audit|
1235 | expect(audit.user).to eq(user.name)
1236 | end
1237 | end
1238 | end
1239 | end
1240 |
1241 | describe "after_audit" do
1242 | let(:user) { Models::ActiveRecord::UserWithAfterAudit.new }
1243 |
1244 | it "should invoke after_audit callback on create" do
1245 | expect(user.bogus_attr).to be_nil
1246 | expect(user.save).to eq(true)
1247 | expect(user.bogus_attr).to eq("do something")
1248 | end
1249 | end
1250 |
1251 | describe "around_audit" do
1252 | let(:user) { Models::ActiveRecord::UserWithAfterAudit.new }
1253 |
1254 | it "should invoke around_audit callback on create" do
1255 | expect(user.around_attr).to be_nil
1256 | expect(user.save).to eq(true)
1257 | expect(user.around_attr).to eq(user.audits.last)
1258 | end
1259 | end
1260 |
1261 | describe "STI auditing" do
1262 | it "should correctly disable auditing when using STI" do
1263 | company = Models::ActiveRecord::Company::STICompany.create name: "The auditors"
1264 | expect(company.type).to eq("Models::ActiveRecord::Company::STICompany")
1265 | expect {
1266 | Models::ActiveRecord::Company.auditing_enabled = false
1267 | company.update! name: "STI auditors"
1268 | Models::ActiveRecord::Company.auditing_enabled = true
1269 | }.to_not change(Audited::Audit, :count)
1270 | end
1271 | end
1272 | end
1273 |
--------------------------------------------------------------------------------