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