diff --git a/.circleci/config.yml b/.circleci/config.yml
index ad745e299..6b485e6c3 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,11 +1,11 @@
version: 2.1
orbs:
- ruby: circleci/ruby@0.1.2
+ ruby: circleci/ruby@2.1.0
jobs:
build:
docker:
- - image: circleci/ruby:2.3-stretch
+ - image: cimg/ruby:3.0.4
executor: ruby/default
steps:
- checkout
diff --git a/.standard.yml b/.standard.yml
new file mode 100644
index 000000000..04cce26aa
--- /dev/null
+++ b/.standard.yml
@@ -0,0 +1,5 @@
+ruby_version: 2.3
+ignore:
+ - lib/generators/audited/templates/**/*
+ - vendor/bundle/**/*
+ - gemfiles/vendor/bundle/**/*
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index aef4846fd..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,58 +0,0 @@
-language: ruby
-cache: bundler
-rvm:
- - 2.3.7
- - 2.4.4
- - 2.5.1
- - 2.6.3
- - ruby-head
-env:
- - DB=SQLITE
- - DB=POSTGRES
- - DB=MYSQL
-addons:
- postgresql: "9.4"
-services:
- - mysql
-before_install:
- # https://github.com/travis-ci/travis-ci/issues/8978
- - "travis_retry gem update --system"
- # Rails 4.2 has a bundler 1.x requirement
- - if [ $BUNDLE_GEMFILE = $PWD/gemfiles/rails42.gemfile ]; then
- rvm @global do gem uninstall bundler -a -x;
- travis_retry gem install -v '< 2.0.0' bundler;
- else
- travis_retry gem install bundler;
- fi
-gemfile:
- - gemfiles/rails42.gemfile
- - gemfiles/rails50.gemfile
- - gemfiles/rails51.gemfile
- - gemfiles/rails52.gemfile
- - gemfiles/rails60.gemfile
-matrix:
- include:
- - rvm: 2.6.3
- script: bundle exec rubocop --parallel
- env: DB=rubocop # make travis build display nicer
- exclude:
- - rvm: 2.3.7
- gemfile: gemfiles/rails60.gemfile
- - rvm: 2.4.4
- gemfile: gemfiles/rails60.gemfile
- - rvm: 2.6.3
- gemfile: gemfiles/rails42.gemfile
- - rvm: ruby-head
- gemfile: gemfiles/rails42.gemfile
- allow_failures:
- - rvm: ruby-head
- fast_finish: true
-branches:
- only:
- - master
- - /.*-stable$/
-notifications:
- webhooks:
- urls:
- - http://buildlight.collectiveidea.com/
- on_start: always
diff --git a/Appraisals b/Appraisals
index 41c9a2356..0211d2418 100644
--- a/Appraisals
+++ b/Appraisals
@@ -23,16 +23,60 @@ appraise 'rails51' do
gem "sqlite3", "~> 1.3.6"
end
-appraise 'rails52' do
- gem 'rails', '>= 5.2.0', '< 5.3'
+appraise "rails52" do
+ gem "rails", "~> 5.2.8"
gem "mysql2", ">= 0.4.4", "< 0.6.0"
gem "pg", ">= 0.18", "< 2.0"
gem "sqlite3", "~> 1.3.6"
+ gem "psych", "~> 3.1"
+ gem "loofah", "2.20.0"
end
-appraise 'rails60' do
- gem 'rails', '>= 6.0.0.rc1', '< 6.1'
+appraise "rails60" do
+ gem "rails", "~> 6.0.6"
gem "mysql2", ">= 0.4.4"
gem "pg", ">= 0.18", "< 2.0"
gem "sqlite3", "~> 1.4"
end
+
+appraise "rails61" do
+ gem "rails", "~> 6.1.7"
+ gem "mysql2", ">= 0.4.4"
+ gem "pg", ">= 1.1", "< 2.0"
+ gem "sqlite3", "~> 1.4"
+end
+
+appraise "rails70" do
+ gem "rails", "~> 7.0.8"
+ gem "mysql2", ">= 0.4.4"
+ gem "pg", ">= 1.1"
+ gem "sqlite3", "~> 1.4"
+end
+
+appraise "rails71" do
+ gem "rails", "~> 7.1.3"
+ gem "mysql2", ">= 0.4.4"
+ gem "pg", ">= 1.1"
+ gem "sqlite3", "~> 1.4"
+end
+
+appraise "rails72" do
+ gem "rails", "~> 7.2.0"
+ gem "mysql2", "~> 0.5"
+ gem "pg", "~> 1.1"
+ gem "sqlite3", ">= 1.4"
+end
+
+appraise "rails80" do
+ gem "rails", "~> 8.0.0"
+ gem "mysql2", "~> 0.5"
+ gem "pg", "~> 1.1"
+ gem "sqlite3", ">= 1.4"
+end
+
+appraise "rails_main" do
+ gem "rails", github: "rails/rails", branch: "main"
+ gem "mysql2", "~> 0.5"
+ gem "pg", "~> 1.1"
+ gem "sqlite3", ">= 2.0"
+end
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c1b20f710..6395c14fe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,193 @@
# Audited ChangeLog
-## Unreleased
+### 5.8.0 (2024-11-08)
+- Allow calling audited multiple times - @mohammednasser-32
+ [734](https://github.com/collectiveidea/audited/pull/734)
+- Relax gemspec to allow Rails 8.1 - @BranLiang
+ [738](https://github.com/collectiveidea/audited/pull/738)
+
+### 5.7.0 (2024-08-13)
+
+- Support for Rails 7.2 and Ruby 3.3, and testing cleanups - @mattbrictson
+ [#723](https://github.com/collectiveidea/audited/pull/723)
+- Allow max_audits to be a proc or symbol - @gmhawash
+ [#718](https://github.com/collectiveidea/audited/pull/718)
+- Support Rails 8 - @fernandomenolli
+ [#717](https://github.com/collectiveidea/audited/pull/717)
+
+
+### 5.6.0 (2024-04-05)
+
+- Removed support for Rails 5.0 and 5.1.
+- Replace RequestStore with ActiveSupport::CurrentAttributes - @punkisdead
+ [#702](https://github.com/collectiveidea/audited/pull/702)
+
+### 5.5.0 (2024-04-02)
+
+- Bad release. Same code as 5.4.1. Use 5.6.0 for updated features.
+
+### 5.4.3 (2024-01-11)
+
+- Ignore readonly columns in audit - @sriddbs
+ [#692](https://github.com/collectiveidea/audited/pull/692)
+- Robustify Rails version checks - @blaet
+ [#689](https://github.com/collectiveidea/audited/pull/689)
+- Ignore callbacks if not specifed on the model
+ [#679](https://github.com/collectiveidea/audited/pull/679)
+
+## 5.4.2 (2023-11-30)
+
+- Revert replacing RequetStore with ActiveSupport::CurrentAttributes until it is fully tested.
+
+## 5.4.1 (2023-11-30)
+
+- Replace RequestStore with ActiveSupport::CurrentAttributes - @the-spectator
+ [#673](https://github.com/collectiveidea/audited/pull/673/)
+- Don't require railtie when used outside of Rails - @nicduke38degrees
+ [#665](https://github.com/collectiveidea/audited/pull/665)
+
+## 5.4.0 (2023-09-30)
+
+- Add Rails 7.1 support - @yuki24
+ [#686](https://github.com/collectiveidea/audited/pull/686)
+
+## 5.3.3 (2023-03-24)
+
+- Use RequestStore instead of Thread.current for thread-safe requests - @tiagocassio
+ [#669](https://github.com/c ollectiveidea/audited/pull/669)
+- Clean up Touch audits - @mcyoung, @akostadinov
+ [#668](https://github.com/collectiveidea/audited/pull/668)
+
+## 5.3.2 (2023-02-22)
+
+- Touch audit bug fixes - @mcyoung
+ [#662](https://github.com/collectiveidea/audited/pull/662)
+
+## 5.3.1 (2023-02-21)
+
+- Ensure touch support doesn't cause double audits - @mcyoung
+ [#660](https://github.com/collectiveidea/audited/pull/660)
+- Testing Improvements - @vlad-psh
+ [#628](https://github.com/collectiveidea/audited/pull/628)
+- Testing Improvements - @mcyoung
+ [#658](https://github.com/collectiveidea/audited/pull/658)
+
+## 5.3.0 (2023-02-14)
+
+- Audit touch calls - @mcyoung
+ [#657](https://github.com/collectiveidea/audited/pull/657)
+- Allow using with Padrino and other non-Rails projects - @nicduke38degrees
+ [#655](https://github.com/collectiveidea/audited/pull/655)
+- Testing updates - @jdufresne
+ [#652](https://github.com/collectiveidea/audited/pull/652)
+ [#653](https://github.com/collectiveidea/audited/pull/653)
+
+## 5.2.0 (2023-01-23)
+
+Improved
+
+- config.audit_class can take a string or constant - @rocket-turtle
+ Fixes overzealous change in 5.1.0 where it only took a string.
+ [#648](https://github.com/collectiveidea/audited/pull/648)
+- README link fix - @jeremiahlukus
+ [#646](https://github.com/collectiveidea/audited/pull/646)
+- Typo fix in GitHub Actions - @jdufresne
+ [#644](https://github.com/collectiveidea/audited/pull/644)
+
+## 5.1.0 (2022-12-23)
+
+Changed
+
+- config.audit_class takes a string - @simmerz
+ [#609](https://github.com/collectiveidea/audited/pull/609)
+- Filter encrypted attributes automatically - @vlad-psh
+ [#630](https://github.com/collectiveidea/audited/pull/630)
+
+Improved
+
+- README improvements - @jess, @mstroming
+ [#605](https://github.com/collectiveidea/audited/pull/605)
+ [#640](https://github.com/collectiveidea/audited/issues/640)
+- Ignore deadlocks in concurrent audit combinations - @Crammaman
+ [#621](https://github.com/collectiveidea/audited/pull/621)
+- Fix timestamped_migrations deprecation warning - @shouichi
+ [#624](https://github.com/collectiveidea/audited/pull/624)
+- Ensure audits are re-enabled after blocks - @dcorlett
+ [#632](https://github.com/collectiveidea/audited/pull/632)
+- Replace raw string where clause with query methods - @macowie
+ [#642](https://github.com/collectiveidea/audited/pull/642)
+- Test against more Ruby/Rails Versions - @enomotodev, @danielmorrison
+ [#610](https://github.com/collectiveidea/audited/pull/610)
+ [#643](https://github.com/collectiveidea/audited/pull/643)
+
+## 5.0.2 (2021-09-16)
+
+Added
+
+- Relax ActiveRecord version constraint to support Rails 7
+ [#597](https://github.com/collectiveidea/audited/pull/597)
+
+Improved
+
+- Improve loading - @mvastola
+ [#592](https://github.com/collectiveidea/audited/pull/592)
+- Update README - @danirod, @clement1234
+ [#596](https://github.com/collectiveidea/audited/pull/596)
+ [#594](https://github.com/collectiveidea/audited/pull/594)
+
+
+## 5.0.1 (2021-06-11)
+
+Improved
+
+- Don't load associated model when auditing is disabled - @nut4k1
+ [#584](https://github.com/collectiveidea/audited/pull/584)
+
+## 5.0.0 (2021-06-10)
+
+Improved
+
+- Fixes an issue where array attributes were not deserialized properly - @cfeckardt, @yuki24
+ [#448](https://github.com/collectiveidea/audited/pull/448)
+ [#576](https://github.com/collectiveidea/audited/pull/576)
+- Improve error message on audit_comment and allow for i18n override - @james
+ [#523](https://github.com/collectiveidea/audited/pull/523/)
+- Don't require a comment if only non-audited fields are changed - @james
+ [#522](https://github.com/collectiveidea/audited/pull/522/)
+- Readme updates - @gourshete
+ [#525](https://github.com/collectiveidea/audited/pull/525)
+- Allow restoring previous enum behavior with flag - @travisofthenorth
+ [#526](https://github.com/collectiveidea/audited/pull/526)
+- Follow Rails Autoloading conventions - @duncanjbrown
+ [#532](https://github.com/collectiveidea/audited/pull/532)
+- Fix own_and_associated_audits for STI Models - @eric-hemasystems
+ [#533](https://github.com/collectiveidea/audited/pull/533)
+- Rails 6.1 Improvements - @okuramasafumi, @marcrohloff
+ [#563](https://github.com/collectiveidea/audited/pull/563)
+ [#544](https://github.com/collectiveidea/audited/pull/544)
+- Use Thread local variables instead of Fibers - @arathunku
+ [#568](https://github.com/collectiveidea/audited/pull/568)
+
+Changed
+
+- Drop support for Rails 4 - @travisofthenorth
+ [#527](https://github.com/collectiveidea/audited/pull/527)
+
+## 4.10.0 (2021-01-07)
+
+Added
+
+- Add redacted option
+ [#485](https://github.com/collectiveidea/audited/pull/485)
+- Rails 6.1. support
+ [#554](https://github.com/collectiveidea/audited/pull/554)
+ [#559](https://github.com/collectiveidea/audited/pull/559)
+
+Improved
+
+- Avoid extra query on first audit version
+ [#513](https://github.com/collectiveidea/audited/pull/513)
+
## 4.9.0 (2019-07-17)
diff --git a/Gemfile b/Gemfile
index 20207658c..2500471eb 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,3 +1,3 @@
-source 'https://rubygems.org'
+source "https://rubygems.org"
gemspec name: "audited"
diff --git a/README.md b/README.md
index 386c19d8f..65a7cb71d 100644
--- a/README.md
+++ b/README.md
@@ -1,20 +1,31 @@
-Audited [](http://travis-ci.org/collectiveidea/audited) [](https://codeclimate.com/github/collectiveidea/audited) [](https://hakiri.io/github/collectiveidea/audited/master)
-=======
+Audited
+[](http://rubygems.org/gems/audited)
+
+[](https://codeclimate.com/github/collectiveidea/audited)
+[](https://github.com/testdouble/standard)
**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.
-Audited currently (4.x) works with Rails 6.0, 5.2, 5.1, 5.0 and 4.2.
+Audited currently (5.6) works with Rails 7.2, 7.1, 7.0, 6.1, 6.0, 5.2.
+
+For Rails 5.0 & 5.1, use gem version 5.4.3
+For Rails 4, use gem version 4.x
For Rails 3, use gem version 3.0 or see the [3.0-stable branch](https://github.com/collectiveidea/audited/tree/3.0-stable).
## Supported Rubies
-Audited supports and is [tested against](http://travis-ci.org/collectiveidea/audited) the following Ruby versions:
+Audited supports and is [tested against](https://github.com/collectiveidea/audited/actions/workflows/ci.yml) the following Ruby versions:
-* 2.3.7
-* 2.4.4
-* 2.5.1
-* 2.6.3
+* 2.3 (only tested on Sqlite due to testing issues with other DBs)
+* 2.4
+* 2.5
+* 2.6
+* 2.7
+* 3.0
+* 3.1
+* 3.2
+* 3.3
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).
@@ -27,7 +38,16 @@ Audited is currently ActiveRecord-only. In a previous life, Audited worked with
Add the gem to your Gemfile:
```ruby
-gem "audited", "~> 4.9"
+gem "audited"
+```
+
+And if you're using ```require: false``` you must add initializers like this:
+
+```ruby
+#./config/initializers/audited.rb
+require "audited"
+
+Audited::Railtie.initializers.each(&:run)
```
Then, from your Rails app directory, create the `audits` table:
@@ -114,18 +134,24 @@ end
### Specifying callbacks
-By default, a new audit is created for any Create, Update or Destroy action. You can, however, limit the actions audited.
+By default, a new audit is created for any Create, Update, Touch (Rails 6+) or Destroy action. You can, however, limit the actions audited.
```ruby
class User < ActiveRecord::Base
# All fields and actions
# audited
- # Single field, only audit Update and Destroy (not Create)
+ # Single field, only audit Update and Destroy (not Create or Touch)
# audited only: :name, on: [:update, :destroy]
end
```
+You can ignore the default callbacks globally unless the callback action is specified in your model using the `:on` option. To configure default callback exclusion, put the following in an initializer file (`config/initializers/audited.rb`):
+
+```ruby
+Audited.ignored_default_callbacks = [:create, :update] # ignore callbacks create and update
+```
+
### Comments
You can attach comments to each audit using an `audit_comment` attribute on your model.
@@ -143,7 +169,7 @@ class User < ActiveRecord::Base
end
```
-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.
+You can update an audit only if audit_comment is present. You can optionally add the `:update_with_comment_only` option set to `false` to your `audited` call to turn this behavior off for all audits.
```ruby
class User < ActiveRecord::Base
@@ -153,7 +179,7 @@ end
### Limiting stored audits
-You can limit the number of audits stored for your model. To configure limiting for all audited models, put the following in an initializer:
+You can limit the number of audits stored for your model. To configure limiting for all audited models, put the following in an initializer file (`config/initializers/audited.rb`):
```ruby
Audited.max_audits = 10 # keep only 10 latest audits
@@ -192,7 +218,7 @@ class PostsController < ApplicationController
end
```
-To use a method other than `current_user`, put the following in an initializer:
+To use a method other than `current_user`, put the following in an initializer file (`config/initializers/audited.rb`):
```ruby
Audited.current_user_method = :authenticated_user
@@ -219,7 +245,7 @@ class ApplicationController < ActionController::Base
if current_user
current_user
else
- 'Elon Musk'
+ 'Alexander Fleming'
end
end
end
@@ -234,6 +260,16 @@ end
post.audits.last.user # => 'console-user-username'
```
+If you want to set a specific user as the auditor of the commands in a CLI environment, whether that is a string or an ActiveRecord object, you can use the following command:
+
+```rb
+Audited.store[:audited_user] = "username"
+
+# or
+
+Audited.store[:audited_user] = User.find(1)
+```
+
### Associated Audits
Sometimes it's useful to associate an audit with a model other than the one being changed. For instance, given the following models:
@@ -258,6 +294,7 @@ class User < ActiveRecord::Base
end
class Company < ActiveRecord::Base
+ audited
has_many :users
has_associated_audits
end
@@ -286,8 +323,6 @@ If you want to audit only under specific conditions, you can provide conditional
class User < ActiveRecord::Base
audited if: :active?
- private
-
def active?
last_login > 6.months.ago
end
@@ -358,6 +393,19 @@ User.auditing_enabled = false
end
```
+### Encrypted attributes
+
+If you're using ActiveRecord's encryption (available from Rails 7) to encrypt some attributes, Audited will automatically filter values of these attributes. No additional configuration is required. Changes to encrypted attributes will be logged as `[FILTERED]`.
+
+If you want to extend or modify the audit model, create a new class that
+inherits from `Audited::Audit`:
+```ruby
+class User < ActiveRecord::Base
+ audited
+ encrypts :password
+end
+```
+
### Custom `Audit` model
If you want to extend or modify the audit model, create a new class that
@@ -374,13 +422,23 @@ Then set it in an initializer:
# config/initializers/audited.rb
Audited.config do |config|
- config.audit_class = CustomAudit
+ config.audit_class = "CustomAudit"
end
```
+### Enum Storage
+
+In 4.10, the default behavior for enums changed from storing the value synthesized by Rails to the value stored in the DB. You can restore the previous behavior by setting the store_synthesized_enums configuration value:
+
+```ruby
+# config/initializers/audited.rb
+
+Audited.store_synthesized_enums = true
+```
+
## Support
-You can find documentation at: http://rdoc.info/github/collectiveidea/audited
+You can find documentation at: https://www.rubydoc.info/gems/audited
Or join the [mailing list](http://groups.google.com/group/audited) to get help or offer suggestions.
diff --git a/Rakefile b/Rakefile
index bacc622ac..9a79ad05e 100755
--- a/Rakefile
+++ b/Rakefile
@@ -1,17 +1,15 @@
#!/usr/bin/env rake
-require 'bundler/gem_helper'
-require 'rspec/core/rake_task'
-require 'rake/testtask'
-require 'appraisal'
-
-Bundler::GemHelper.install_tasks(name: 'audited')
+require "bundler/gem_tasks"
+require "rspec/core/rake_task"
+require "rake/testtask"
+require "appraisal"
RSpec::Core::RakeTask.new(:spec)
Rake::TestTask.new do |t|
t.libs << "test"
- t.test_files = FileList['test/**/*_test.rb']
+ t.test_files = FileList["test/**/*_test.rb"]
t.verbose = true
end
diff --git a/audited.gemspec b/audited.gemspec
index 43bfced03..78fb8a67b 100644
--- a/audited.gemspec
+++ b/audited.gemspec
@@ -1,4 +1,3 @@
-# encoding: utf-8
$:.push File.expand_path("../lib", __FILE__)
require "audited/version"
@@ -6,33 +5,34 @@ Gem::Specification.new do |gem|
gem.name = 'audited'
gem.version = Audited::VERSION
- gem.authors = ['Brandon Keepers', 'Kenneth Kalmer', 'Daniel Morrison', 'Brian Ryckbost', 'Steve Richert', 'Ryan Glover']
- gem.email = 'info@collectiveidea.com'
- gem.description = 'Log all changes to your models'
- gem.summary = gem.description
- gem.homepage = 'https://github.com/collectiveidea/audited'
- gem.license = 'MIT'
+ gem.authors = ["Brandon Keepers", "Kenneth Kalmer", "Daniel Morrison", "Brian Ryckbost", "Steve Richert", "Ryan Glover"]
+ gem.email = "info@collectiveidea.com"
+ gem.description = "Log all changes to your models"
+ gem.summary = gem.description
+ gem.homepage = "https://github.com/collectiveidea/audited"
+ gem.license = "MIT"
- gem.files = `git ls-files`.split($\).reject{|f| f =~ /(\.gemspec)/ }
+ gem.files = `git ls-files`.split($\).reject { |f| f =~ /^(\.gemspec|\.git|\.standard|\.yard|gemfiles|test|spec)/ }
- gem.required_ruby_version = '>= 2.3.0'
+ gem.required_ruby_version = ">= 2.3.0"
- gem.add_dependency 'activerecord', '>= 4.2', '< 6.1'
+ gem.add_dependency "activerecord", ">= 5.2", "< 8.2"
+ gem.add_dependency "activesupport", ">= 5.2", "< 8.2"
- gem.add_development_dependency 'appraisal'
- gem.add_development_dependency 'rails', '>= 4.2', '< 6.1'
- gem.add_development_dependency 'rubocop', '~> 0.54.0'
- gem.add_development_dependency 'rspec-rails', '~> 3.5'
- gem.add_development_dependency 'single_cov'
+ gem.add_development_dependency "appraisal"
+ gem.add_development_dependency "rails", ">= 5.2", "< 8.2"
+ gem.add_development_dependency "rspec-rails"
+ gem.add_development_dependency "standard"
+ gem.add_development_dependency "single_cov"
# JRuby support for the test ENV
if defined?(JRUBY_VERSION)
- gem.add_development_dependency 'activerecord-jdbcsqlite3-adapter', '~> 1.3'
- gem.add_development_dependency 'activerecord-jdbcpostgresql-adapter', '~> 1.3'
- gem.add_development_dependency 'activerecord-jdbcmysql-adapter', '~> 1.3'
+ gem.add_development_dependency "activerecord-jdbcsqlite3-adapter", "~> 1.3"
+ gem.add_development_dependency "activerecord-jdbcpostgresql-adapter", "~> 1.3"
+ gem.add_development_dependency "activerecord-jdbcmysql-adapter", "~> 1.3"
else
- gem.add_development_dependency 'sqlite3', '~> 1.3'
- gem.add_development_dependency 'mysql2', '>= 0.3.20'
- gem.add_development_dependency 'pg', '>= 0.18', '< 2.0'
+ gem.add_development_dependency "sqlite3", ">= 1.3.6"
+ gem.add_development_dependency "mysql2", ">= 0.3.20"
+ gem.add_development_dependency "pg", ">= 0.18", "< 2.0"
end
end
diff --git a/gemfiles/rails42.gemfile b/gemfiles/rails42.gemfile
deleted file mode 100644
index 91e25565c..000000000
--- a/gemfiles/rails42.gemfile
+++ /dev/null
@@ -1,11 +0,0 @@
-# This file was generated by Appraisal
-
-source "https://rubygems.org"
-
-gem "rails", "~> 4.2.0"
-gem "protected_attributes"
-gem "mysql2", ">= 0.3.13", "< 0.6.0"
-gem "pg", "~> 0.15"
-gem "sqlite3", "~> 1.3.6"
-
-gemspec name: "audited", path: "../"
diff --git a/gemfiles/rails50.gemfile b/gemfiles/rails50.gemfile
deleted file mode 100644
index 9a14d0c24..000000000
--- a/gemfiles/rails50.gemfile
+++ /dev/null
@@ -1,10 +0,0 @@
-# This file was generated by Appraisal
-
-source "https://rubygems.org"
-
-gem "rails", "~> 5.0.0"
-gem "mysql2", ">= 0.3.18", "< 0.6.0"
-gem "pg", ">= 0.18", "< 2.0"
-gem "sqlite3", "~> 1.3.6"
-
-gemspec name: "audited", path: "../"
diff --git a/gemfiles/rails52.gemfile b/gemfiles/rails52.gemfile
index 3fcdabaa8..5b4e53b10 100644
--- a/gemfiles/rails52.gemfile
+++ b/gemfiles/rails52.gemfile
@@ -2,9 +2,11 @@
source "https://rubygems.org"
-gem "rails", ">= 5.2.0", "< 5.3"
+gem "rails", "~> 5.2.8"
gem "mysql2", ">= 0.4.4", "< 0.6.0"
gem "pg", ">= 0.18", "< 2.0"
gem "sqlite3", "~> 1.3.6"
+gem "psych", "~> 3.1"
+gem "loofah", "2.20.0"
gemspec name: "audited", path: "../"
diff --git a/gemfiles/rails60.gemfile b/gemfiles/rails60.gemfile
index d514ebce7..b182f5346 100644
--- a/gemfiles/rails60.gemfile
+++ b/gemfiles/rails60.gemfile
@@ -2,7 +2,7 @@
source "https://rubygems.org"
-gem "rails", ">= 6.0.0.rc1", "< 6.1"
+gem "rails", "~> 6.0.6"
gem "mysql2", ">= 0.4.4"
gem "pg", ">= 0.18", "< 2.0"
gem "sqlite3", "~> 1.4"
diff --git a/gemfiles/rails61.gemfile b/gemfiles/rails61.gemfile
new file mode 100644
index 000000000..7fa518869
--- /dev/null
+++ b/gemfiles/rails61.gemfile
@@ -0,0 +1,10 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org"
+
+gem "rails", "~> 6.1.7"
+gem "mysql2", ">= 0.4.4"
+gem "pg", ">= 1.1", "< 2.0"
+gem "sqlite3", "~> 1.4"
+
+gemspec name: "audited", path: "../"
diff --git a/gemfiles/rails70.gemfile b/gemfiles/rails70.gemfile
new file mode 100644
index 000000000..592d2cd8d
--- /dev/null
+++ b/gemfiles/rails70.gemfile
@@ -0,0 +1,10 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org"
+
+gem "rails", "~> 7.0.8"
+gem "mysql2", ">= 0.4.4"
+gem "pg", ">= 1.1"
+gem "sqlite3", "~> 1.4"
+
+gemspec name: "audited", path: "../"
diff --git a/gemfiles/rails71.gemfile b/gemfiles/rails71.gemfile
new file mode 100644
index 000000000..90e3e25bd
--- /dev/null
+++ b/gemfiles/rails71.gemfile
@@ -0,0 +1,10 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org"
+
+gem "rails", "~> 7.1.3"
+gem "mysql2", ">= 0.4.4"
+gem "pg", ">= 1.1"
+gem "sqlite3", "~> 1.4"
+
+gemspec name: "audited", path: "../"
diff --git a/gemfiles/rails72.gemfile b/gemfiles/rails72.gemfile
new file mode 100644
index 000000000..52cd9d17a
--- /dev/null
+++ b/gemfiles/rails72.gemfile
@@ -0,0 +1,10 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org"
+
+gem "rails", "~> 7.2.0"
+gem "mysql2", "~> 0.5"
+gem "pg", "~> 1.1"
+gem "sqlite3", ">= 1.4"
+
+gemspec name: "audited", path: "../"
diff --git a/gemfiles/rails80.gemfile b/gemfiles/rails80.gemfile
new file mode 100644
index 000000000..2942329f9
--- /dev/null
+++ b/gemfiles/rails80.gemfile
@@ -0,0 +1,10 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org"
+
+gem "rails", "~> 8.0.0"
+gem "mysql2", "~> 0.5"
+gem "pg", "~> 1.1"
+gem "sqlite3", ">= 1.4"
+
+gemspec name: "audited", path: "../"
diff --git a/gemfiles/rails_main.gemfile b/gemfiles/rails_main.gemfile
new file mode 100644
index 000000000..c0bd890a5
--- /dev/null
+++ b/gemfiles/rails_main.gemfile
@@ -0,0 +1,10 @@
+# This file was generated by Appraisal
+
+source "https://rubygems.org"
+
+gem "rails", github: "rails/rails", branch: "main"
+gem "mysql2", "~> 0.5"
+gem "pg", "~> 1.1"
+gem "sqlite3", ">= 2.0"
+
+gemspec name: "audited", path: "../"
diff --git a/lib/audited-rspec.rb b/lib/audited-rspec.rb
index c977fd9e8..663fb4db8 100644
--- a/lib/audited-rspec.rb
+++ b/lib/audited-rspec.rb
@@ -1,4 +1,6 @@
-require 'audited/rspec_matchers'
+# frozen_string_literal: true
+
+require "audited/rspec_matchers"
module RSpec::Matchers
include Audited::RspecMatchers
end
diff --git a/lib/audited.rb b/lib/audited.rb
index 86581ef02..dc1374867 100644
--- a/lib/audited.rb
+++ b/lib/audited.rb
@@ -1,23 +1,46 @@
-require 'active_record'
+# frozen_string_literal: true
+
+require "active_record"
module Audited
+ # Wrapper around ActiveSupport::CurrentAttributes
+ class RequestStore < ActiveSupport::CurrentAttributes
+ attribute :audited_store
+ end
+
class << self
- attr_accessor :ignored_attributes, :current_user_method, :max_audits, :auditing_enabled,
- :namespace_conditions
+ attr_accessor \
+ :auditing_enabled,
+ :current_user_method,
+ :ignored_attributes,
+ :ignored_default_callbacks,
+ :max_audits,
+ :store_synthesized_enums,
+ :namespace_conditions
+
attr_writer :audit_class
def audit_class
- @audit_class ||= Audit
+ # The audit_class is set as String in the initializer. It can not be constantized during initialization and must
+ # be constantized at runtime. See https://github.com/collectiveidea/audited/issues/608
+ @audit_class = @audit_class.safe_constantize if @audit_class.is_a?(String)
+ @audit_class ||= Audited::Audit
end
+ # remove audit_model in next major version it was only shortly present in 5.1.0
+ alias_method :audit_model, :audit_class
+ deprecate audit_model: "use Audited.audit_class instead of Audited.audit_model. This method will be removed.",
+ deprecator: ActiveSupport::Deprecation.new('6.0.0', 'Audited')
+
def store
- Thread.current[:audited_store] ||= {}
+ RequestStore.audited_store ||= {}
end
def config
yield(self)
end
+ # Fundthrough internal code to create the audits table with service_name column
def dev_test_schema
proc do
create_table :audits do |t|
@@ -42,16 +65,21 @@ def dev_test_schema
end
end
- @ignored_attributes = %w(lock_version created_at updated_at created_on updated_on)
+ @ignored_attributes = %w[lock_version created_at updated_at created_on updated_on]
+ @ignored_default_callbacks = []
@namespace_conditions = {}
@current_user_method = :current_user
@auditing_enabled = true
+ @store_synthesized_enums = false
end
-require 'audited/auditor'
-require 'audited/audit'
+require "audited/auditor"
-::ActiveRecord::Base.send :include, Audited::Auditor
+ActiveSupport.on_load :active_record do
+ require "audited/audit"
+ include Audited::Auditor
+end
-require 'audited/sweeper'
+require "audited/sweeper"
+require "audited/railtie" if Audited.const_defined?(:Rails)
diff --git a/lib/audited/audit.rb b/lib/audited/audit.rb
index 38ea795bd..28b45ecf3 100644
--- a/lib/audited/audit.rb
+++ b/lib/audited/audit.rb
@@ -1,4 +1,6 @@
-require 'set'
+# frozen_string_literal: true
+
+require "set"
module Audited
# Audit saves the changes to ActiveRecord models. It has the following attributes:
@@ -16,7 +18,7 @@ module Audited
class YAMLIfTextColumnType
class << self
def load(obj)
- if Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
+ if text_column?
ActiveRecord::Coders::YAMLColumn.new(Object).load(obj)
else
obj
@@ -24,21 +26,26 @@ def load(obj)
end
def dump(obj)
- if Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
+ if text_column?
ActiveRecord::Coders::YAMLColumn.new(Object).dump(obj)
else
obj
end
end
+
+ def text_column?
+ Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
+ end
end
end
class Audit < ::ActiveRecord::Base
- belongs_to :auditable, polymorphic: true
- belongs_to :user, polymorphic: true
+ belongs_to :auditable, polymorphic: true
+ belongs_to :user, polymorphic: true
belongs_to :associated, polymorphic: true
before_create :set_version_number, :set_audit_user, :set_request_uuid, :set_remote_address
+ # Fundthrough internal code to allow setting namespace conditions from config, currently used to set `service_name` value
before_create do
self.assign_attributes(::Audited.namespace_conditions)
end
@@ -46,13 +53,17 @@ class Audit < ::ActiveRecord::Base
cattr_accessor :audited_class_names
self.audited_class_names = Set.new
- serialize :audited_changes, YAMLIfTextColumnType
+ if Rails.gem_version >= Gem::Version.new("7.1")
+ serialize :audited_changes, coder: YAMLIfTextColumnType
+ else
+ serialize :audited_changes, YAMLIfTextColumnType
+ end
- scope :ascending, ->{ reorder(version: :asc) }
- scope :descending, ->{ reorder(version: :desc)}
- scope :creates, ->{ where(action: 'create')}
- scope :updates, ->{ where(action: 'update')}
- scope :destroys, ->{ where(action: 'destroy')}
+ scope :ascending, -> { reorder(version: :asc) }
+ scope :descending, -> { reorder(version: :desc) }
+ scope :creates, -> { where(action: "create") }
+ scope :updates, -> { where(action: "update") }
+ scope :destroys, -> { where(action: "destroy") }
scope :namespaced, ->{ where(Audited.namespace_conditions)}
scope :not_before_created_at, ->(audited_record) do
@@ -61,10 +72,11 @@ class Audit < ::ActiveRecord::Base
(Time.now + 1.day)
))
end
- scope :up_until, ->(date_or_time){ where("created_at <= ?", date_or_time) }
- scope :from_version, ->(version){ where('version >= ?', version) }
- scope :to_version, ->(version){ where('version <= ?', version) }
- scope :auditable_finder, ->(auditable_id, auditable_type){ namespaced.where(auditable_id: auditable_id, auditable_type: auditable_type)}
+
+ scope :up_until, ->(date_or_time) { where("created_at <= ?", date_or_time) }
+ scope :from_version, ->(version) { where("version >= ?", version) }
+ scope :to_version, ->(version) { where("version <= ?", version) }
+ scope :auditable_finder, ->(auditable_id, auditable_type) { namespaced.where(auditable_id: auditable_id, auditable_type: auditable_type) }
# Return all audits older than the current one.
def ancestors
self.class.ascending.auditable_finder(auditable_id, auditable_type).to_version(version)
@@ -81,18 +93,32 @@ def revision
# Returns a hash of the changed attributes with the new values
def new_attributes
- (audited_changes || {}).inject({}.with_indifferent_access) do |attrs, (attr, values)|
- attrs[attr] = values.is_a?(Array) ? values.last : values
- attrs
+ (audited_changes || {}).each_with_object({}.with_indifferent_access) do |(attr, values), attrs|
+ attrs[attr] = (action == "update") ? values.last : values
end
end
# Returns a hash of the changed attributes with the old values
def old_attributes
- (audited_changes || {}).inject({}.with_indifferent_access) do |attrs, (attr, values)|
- attrs[attr] = Array(values).first
+ (audited_changes || {}).each_with_object({}.with_indifferent_access) do |(attr, values), attrs|
+ attrs[attr] = (action == "update") ? values.first : values
+ end
+ end
- attrs
+ # Allows user to undo changes
+ def undo
+ case action
+ when "create"
+ # destroys a newly created record
+ auditable.destroy!
+ when "destroy"
+ # creates a new record with the destroyed record attributes
+ auditable_type.constantize.create!(audited_changes)
+ when "update"
+ # changes back attributes
+ auditable.update!(audited_changes.transform_values(&:first))
+ else
+ raise StandardError, "invalid action given #{action}"
end
end
@@ -178,8 +204,13 @@ def self.collection_cache_key(collection = all, *)
private
def set_version_number
- max = self.class.namespaced.not_before_created_at(auditable).auditable_finder(auditable_id, auditable_type).maximum(:version) || 0
- self.version = max + 1
+ if action == "create"
+ self.version = 1
+ else
+ collection = (ActiveRecord::VERSION::MAJOR >= 6) ? self.class.unscoped : self.class
+ max = collection.namespaced.not_before_created_at(auditable).auditable_finder(auditable_id, auditable_type).maximum(:version) || 0
+ self.version = max + 1
+ end
end
def set_audit_user
diff --git a/lib/audited/auditor.rb b/lib/audited/auditor.rb
index 549bcd1d5..3f881ddc2 100644
--- a/lib/audited/auditor.rb
+++ b/lib/audited/auditor.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Audited
# Specify this act if you want changes to your model to be saved in an
# audit table. This assumes there is an audits table ready.
@@ -11,7 +13,7 @@ module Audited
#
# See Audited::Auditor::ClassMethods#audited
# for configuration options
- module Auditor #:nodoc:
+ module Auditor # :nodoc:
extend ActiveSupport::Concern
CALLBACKS = [:audit_create, :audit_update, :audit_destroy]
@@ -34,6 +36,16 @@ module ClassMethods
# * +require_comment+ - Ensures that audit_comment is supplied before
# any create, update or destroy operation.
# * +max_audits+ - Limits the number of stored audits.
+
+ # * +redacted+ - Changes to these fields will be logged, but the values
+ # will not. This is useful, for example, if you wish to audit when a
+ # password is changed, without saving the actual password in the log.
+ # To store values as something other than '[REDACTED]', pass an argument
+ # to the redaction_value option.
+ #
+ # class User < ActiveRecord::Base
+ # audited redacted: :password, redaction_value: SecureRandom.uuid
+ # end
#
# * +if+ - Only audit the model when the given function returns true
# * +unless+ - Only audit the model when the given function returns false
@@ -47,36 +59,48 @@ module ClassMethods
# end
#
def audited(options = {})
- # don't allow multiple calls
- return if included_modules.include?(Audited::Auditor::AuditedInstanceMethods)
+ audited? ? update_audited_options(options) : set_audit(options)
+ end
+
+ private
+ def audited?
+ included_modules.include?(Audited::Auditor::AuditedInstanceMethods)
+ end
+
+ def set_audit(options)
extend Audited::Auditor::AuditedClassMethods
include Audited::Auditor::AuditedInstanceMethods
class_attribute :audit_associated_with, instance_writer: false
- class_attribute :audited_options, instance_writer: false
+ class_attribute :audited_options, instance_writer: false
attr_accessor :audit_version, :audit_comment
- self.audited_options = options
- normalize_audited_options
-
- self.audit_associated_with = audited_options[:associated_with]
+ set_audited_options(options)
if audited_options[:comment_required]
validate :presence_of_audit_comment
before_destroy :require_comment if audited_options[:on].include?(:destroy)
end
- attr_accessor :audit_comment
-
has_many :audits, ->(audited_record) do
- namespaced.not_before_created_at(audited_record).order(version: :asc)
- end, as: :auditable, class_name: Audited.audit_class.name, inverse_of: :auditable
-
+ where(
+ service_name: if Rails.gem_version >= Gem::Version.new("6.0")
+ Rails.application.class.module_parent_name
+ else
+ Rails.application.class.parent_name
+ end,
+ created_at: Range.new(
+ ((audited_record.try(:created_at) || Time.now) - 1.day),
+ (Time.now + 1.day)
+ )
+ ).order(version: :asc)
+ end, as: :auditable, class_name: Audited.audit_class.name
Audited.audit_class.audited_class_names << to_s
- after_create :audit_create if audited_options[:on].include?(:create)
- before_update :audit_update if audited_options[:on].include?(:update)
+ after_create :audit_create if audited_options[:on].include?(:create)
+ before_update :audit_update if audited_options[:on].include?(:update)
+ after_touch :audit_touch if audited_options[:on].include?(:touch) && ::ActiveRecord::VERSION::MAJOR >= 6
before_destroy :audit_destroy if audited_options[:on].include?(:destroy)
# Define and set after_audit and around_audit callbacks. This might be useful if you want
@@ -92,18 +116,22 @@ def audited(options = {})
def has_associated_audits
has_many :associated_audits, as: :associated, class_name: Audited.audit_class.name
end
+
+ def update_audited_options(new_options)
+ previous_audit_options = self.audited_options
+ set_audited_options(new_options)
+ self.reset_audited_columns
+ end
+
+ def set_audited_options(options)
+ self.audited_options = options
+ normalize_audited_options
+ self.audit_associated_with = audited_options[:associated_with]
+ end
end
module AuditedInstanceMethods
- # Deprecate version attribute in favor of audit_version attribute – preparing for eventual removal.
- def method_missing(method_name, *args, &block)
- if method_name == :version
- ActiveSupport::Deprecation.warn("`version` attribute has been changed to `audit_version`. This attribute will be removed.")
- audit_version
- else
- super
- end
- end
+ REDACTED = "[REDACTED]"
# Temporarily turns off auditing while saving.
def save_without_auditing
@@ -145,7 +173,7 @@ def with_auditing(&block)
def revisions(from_version = 1)
return [] unless audits.from_version(from_version).exists?
- all_audits = audits.select([:audited_changes, :version]).to_a
+ all_audits = audits.select([:audited_changes, :version, :action]).to_a
targeted_audits = all_audits.select { |audit| audit.version >= from_version }
previous_attributes = reconstruct_attributes(all_audits - targeted_audits)
@@ -159,7 +187,7 @@ def revisions(from_version = 1)
# Get a specific revision specified by the version number, or +:previous+
# Returns nil for versions greater than revisions count
def revision(version)
- if version == :previous || self.audits.last.version >= version
+ if version == :previous || audits.last.version >= version
revision_with Audited.audit_class.reconstruct_attributes(audits_to(version))
end
end
@@ -173,15 +201,16 @@ def revision_at(date_or_time)
# List of attributes that are audited.
def audited_attributes
audited_attributes = attributes.except(*self.class.non_audited_columns)
+ audited_attributes = redact_values(audited_attributes)
+ audited_attributes = filter_encrypted_attrs(audited_attributes)
normalize_enum_changes(audited_attributes)
end
# Returns a list combined of record audits and associated audits.
def own_and_associated_audits
- Audited.audit_class.unscoped
- .where('(auditable_type = :type AND auditable_id = :id) OR (associated_type = :type AND associated_id = :id)',
- type: self.class.name, id: id)
- .order(created_at: :desc)
+ Audited.audit_class.unscoped.where(auditable: self)
+ .or(Audited.audit_class.unscoped.where(associated: self))
+ .order(created_at: :desc)
end
# Combine multiple audits into one.
@@ -191,8 +220,13 @@ def combine_audits(audits_to_combine)
combine_target.comment = "#{combine_target.comment}\nThis audit is the result of multiple audits being combined."
transaction do
- combine_target.save!
- audits_to_combine.unscope(:limit).where("version < ?", combine_target.version).delete_all
+ begin
+ combine_target.save!
+ audits_to_combine.unscope(:limit).where("version < ?", combine_target.version).delete_all
+ rescue ActiveRecord::Deadlocked
+ # Ignore Deadlocks, if the same record is getting its old audits combined more than once at the same time then
+ # both combining operations will be the same. Ignoring this error allows one of the combines to go through successfully.
+ end
end
end
@@ -201,12 +235,12 @@ def combine_audits(audits_to_combine)
def revision_with(attributes)
dup.tap do |revision|
revision.id = id
- revision.send :instance_variable_set, '@new_record', destroyed?
- revision.send :instance_variable_set, '@persisted', !destroyed?
- revision.send :instance_variable_set, '@readonly', false
- revision.send :instance_variable_set, '@destroyed', false
- revision.send :instance_variable_set, '@_destroyed', false
- revision.send :instance_variable_set, '@marked_for_destruction', false
+ revision.send :instance_variable_set, "@new_record", destroyed?
+ revision.send :instance_variable_set, "@persisted", !destroyed?
+ revision.send :instance_variable_set, "@readonly", false
+ revision.send :instance_variable_set, "@destroyed", false
+ revision.send :instance_variable_set, "@_destroyed", false
+ revision.send :instance_variable_set, "@marked_for_destruction", false
Audited.audit_class.assign_revision_attributes(revision, attributes)
# Remove any association proxies so that they will be recreated
@@ -225,8 +259,17 @@ def revision_with(attributes)
private
- def audited_changes
- all_changes = respond_to?(:changes_to_save) ? changes_to_save : changes
+ def audited_changes(for_touch: false, exclude_readonly_attrs: false)
+ all_changes = if for_touch
+ previous_changes
+ elsif respond_to?(:changes_to_save)
+ changes_to_save
+ else
+ changes
+ end
+
+ all_changes = all_changes.except(*self.class.readonly_attributes.to_a) if exclude_readonly_attrs
+
filtered_changes = \
if audited_options[:only].present?
all_changes.slice(*self.class.audited_columns)
@@ -235,16 +278,28 @@ def audited_changes
end
filtered_changes = normalize_enum_changes(filtered_changes)
+
+ if for_touch && (last_audit = audits.last&.audited_changes)
+ filtered_changes.reject! do |k, v|
+ last_audit[k].to_json == v.to_json ||
+ last_audit[k].to_json == v[1].to_json
+ end
+ end
+
+ filtered_changes = redact_values(filtered_changes)
+ filtered_changes = filter_encrypted_attrs(filtered_changes)
filtered_changes.to_hash
end
def normalize_enum_changes(changes)
+ return changes if Audited.store_synthesized_enums
+
self.class.defined_enums.each do |name, values|
if changes.has_key?(name)
changes[name] = \
if changes[name].is_a?(Array)
changes[name].map { |v| values[v] }
- elsif rails_below?('5.0')
+ elsif rails_below?("5.0")
changes[name]
else
values[changes[name]]
@@ -254,47 +309,92 @@ def normalize_enum_changes(changes)
changes
end
+ def redact_values(filtered_changes)
+ filter_attr_values(
+ audited_changes: filtered_changes,
+ attrs: Array(audited_options[:redacted]).map(&:to_s),
+ placeholder: audited_options[:redaction_value] || REDACTED
+ )
+ end
+
+ def filter_encrypted_attrs(filtered_changes)
+ filter_attr_values(
+ audited_changes: filtered_changes,
+ attrs: respond_to?(:encrypted_attributes) ? Array(encrypted_attributes).map(&:to_s) : []
+ )
+ end
+
+ # Replace values for given attrs to a placeholder and return modified hash
+ #
+ # @param audited_changes [Hash] Hash of changes to be saved to audited version record
+ # @param attrs [Array] Array of attrs, values of which will be replaced to placeholder value
+ # @param placeholder [String] Placeholder to replace original attr values
+ def filter_attr_values(audited_changes: {}, attrs: [], placeholder: "[FILTERED]")
+ attrs.each do |attr|
+ next unless audited_changes.key?(attr)
+
+ changes = audited_changes[attr]
+ values = changes.is_a?(Array) ? changes.map { placeholder } : placeholder
+
+ audited_changes[attr] = values
+ end
+
+ audited_changes
+ end
+
def rails_below?(rails_version)
Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new(rails_version)
end
def audits_to(version = nil)
if version == :previous
- version = if self.audit_version
- self.audit_version - 1
- else
- previous = audits.descending.offset(1).first
- previous ? previous.version : 1
- end
+ version = if audit_version
+ audit_version - 1
+ else
+ previous = audits.descending.offset(1).first
+ previous ? previous.version : 1
+ end
end
audits.to_version(version)
end
def audit_create
- write_audit(action: 'create', audited_changes: audited_attributes,
- comment: audit_comment)
+ write_audit(action: "create", audited_changes: audited_attributes,
+ comment: audit_comment)
end
def audit_update
- unless (changes = audited_changes).empty? && (audit_comment.blank? || audited_options[:update_with_comment_only] == false)
- write_audit(action: 'update', audited_changes: changes,
- comment: audit_comment)
+ unless (changes = audited_changes(exclude_readonly_attrs: true)).empty? && (audit_comment.blank? || audited_options[:update_with_comment_only] == false)
+ write_audit(action: "update", audited_changes: changes,
+ comment: audit_comment)
+ end
+ end
+
+ def audit_touch
+ unless (changes = audited_changes(for_touch: true, exclude_readonly_attrs: true)).empty?
+ write_audit(action: "update", audited_changes: changes,
+ comment: audit_comment)
end
end
def audit_destroy
- write_audit(action: 'destroy', audited_changes: audited_attributes,
- comment: audit_comment) unless new_record?
+ unless new_record?
+ write_audit(action: "destroy", audited_changes: audited_attributes,
+ comment: audit_comment)
+ end
end
def write_audit(attrs)
- attrs[:associated] = send(audit_associated_with) unless audit_associated_with.nil?
+ # return if Rails.env.test?
+
self.audit_comment = nil
if auditing_enabled
+ attrs[:associated] = send(audit_associated_with) unless audit_associated_with.nil?
+
run_callbacks(:audit) {
audit = audits.create(attrs)
- combine_audits_if_needed if attrs[:action] != 'create'
+ combine_audits_if_needed if attrs[:action] != "create"
audit
}
end
@@ -302,28 +402,40 @@ def write_audit(attrs)
def presence_of_audit_comment
if comment_required_state?
- errors.add(:audit_comment, "Comment can't be blank!") unless audit_comment.present?
+ errors.add(:audit_comment, :blank) unless audit_comment.present?
end
end
def comment_required_state?
auditing_enabled &&
- ((audited_options[:on].include?(:create) && self.new_record?) ||
- (audited_options[:on].include?(:update) && self.persisted? && self.changed?))
+ audited_changes.present? &&
+ ((audited_options[:on].include?(:create) && new_record?) ||
+ (audited_options[:on].include?(:update) && persisted? && changed?))
end
def combine_audits_if_needed
- max_audits = audited_options[:max_audits]
+ max_audits = evaluate_max_audits
+
if max_audits && (extra_count = audits.count - max_audits) > 0
audits_to_combine = audits.limit(extra_count + 1)
combine_audits(audits_to_combine)
end
end
+ def evaluate_max_audits
+ max_audits = case (option = audited_options[:max_audits])
+ when Proc then option.call
+ when Symbol then send(option)
+ else
+ option
+ end
+
+ Integer(max_audits).abs if max_audits
+ end
+
def require_comment
if auditing_enabled && audit_comment.blank?
- errors.add(:audit_comment, "Comment can't be blank!")
- return false if Rails.version.start_with?('4.')
+ errors.add(:audit_comment, :blank)
throw(:abort)
end
end
@@ -333,7 +445,7 @@ def require_comment
end
def auditing_enabled
- return run_conditional_check(audited_options[:if]) &&
+ run_conditional_check(audited_options[:if]) &&
run_conditional_check(audited_options[:unless], matching: false) &&
self.class.auditing_enabled
end
@@ -351,7 +463,7 @@ def reconstruct_attributes(audits)
audits.each { |audit| attributes.merge!(audit.new_attributes) }
attributes
end
- end # InstanceMethods
+ end
module AuditedClassMethods
# Returns an array of columns that are audited. See non_audited_columns
@@ -376,7 +488,7 @@ def non_audited_columns=(columns)
# end
#
def without_auditing
- auditing_was_enabled = auditing_enabled
+ auditing_was_enabled = class_auditing_enabled
disable_auditing
yield
ensure
@@ -390,7 +502,7 @@ def without_auditing
# end
#
def with_auditing
- auditing_was_enabled = auditing_enabled
+ auditing_was_enabled = class_auditing_enabled
enable_auditing
yield
ensure
@@ -414,7 +526,7 @@ def audit_as(user, &block)
end
def auditing_enabled
- Audited.store.fetch("#{table_name}_auditing_enabled", true) && Audited.auditing_enabled
+ class_auditing_enabled && Audited.auditing_enabled
end
def auditing_enabled=(val)
@@ -429,11 +541,10 @@ def default_ignored_attributes
def normalize_audited_options
audited_options[:on] = Array.wrap(audited_options[:on])
- audited_options[:on] = [:create, :update, :destroy] if audited_options[:on].empty?
+ audited_options[:on] = ([:create, :update, :touch, :destroy] - Audited.ignored_default_callbacks) if audited_options[:on].empty?
audited_options[:only] = Array.wrap(audited_options[:only]).map(&:to_s)
audited_options[:except] = Array.wrap(audited_options[:except]).map(&:to_s)
- max_audits = audited_options[:max_audits] || Audited.max_audits
- audited_options[:max_audits] = Integer(max_audits).abs if max_audits
+ audited_options[:max_audits] ||= Audited.max_audits
end
def calculate_non_audited_columns
@@ -445,6 +556,15 @@ def calculate_non_audited_columns
default_ignored_attributes
end
end
+
+ def class_auditing_enabled
+ Audited.store.fetch("#{table_name}_auditing_enabled", true)
+ end
+
+ def reset_audited_columns
+ @audited_columns = nil
+ @non_audited_columns = nil
+ end
end
end
end
diff --git a/lib/audited/railtie.rb b/lib/audited/railtie.rb
new file mode 100644
index 000000000..a08311b83
--- /dev/null
+++ b/lib/audited/railtie.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Audited
+ class Railtie < Rails::Railtie
+ initializer "audited.sweeper" do
+ ActiveSupport.on_load(:action_controller) do
+ if defined?(ActionController::Base)
+ ActionController::Base.around_action Audited::Sweeper.new
+ end
+ if defined?(ActionController::API)
+ ActionController::API.around_action Audited::Sweeper.new
+ end
+ end
+ end
+ end
+end
diff --git a/lib/audited/rspec_matchers.rb b/lib/audited/rspec_matchers.rb
index fbb728298..1999068c6 100644
--- a/lib/audited/rspec_matchers.rb
+++ b/lib/audited/rspec_matchers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Audited
module RspecMatchers
# Ensure that the model is audited.
@@ -78,9 +80,9 @@ def negative_failure_message
def description
description = "audited"
description += " associated with #{@options[:associated_with]}" if @options.key?(:associated_with)
- description += " only => #{@options[:only].join ', '}" if @options.key?(:only)
- description += " except => #{@options[:except].join(', ')}" if @options.key?(:except)
- description += " requires audit_comment" if @options.key?(:comment_required)
+ description += " only => #{@options[:only].join ", "}" if @options.key?(:only)
+ description += " except => #{@options[:except].join(", ")}" if @options.key?(:except)
+ description += " requires audit_comment" if @options.key?(:comment_required)
description
end
diff --git a/lib/audited/sweeper.rb b/lib/audited/sweeper.rb
index aca68e36f..c0eba0598 100644
--- a/lib/audited/sweeper.rb
+++ b/lib/audited/sweeper.rb
@@ -1,16 +1,18 @@
+# frozen_string_literal: true
+
module Audited
class Sweeper
STORED_DATA = {
current_remote_address: :remote_ip,
current_request_uuid: :request_uuid,
- current_user: :current_user
+ current_user: :current_user,
}
delegate :store, to: ::Audited
def around(controller)
self.controller = controller
- STORED_DATA.each { |k,m| store[k] = send(m) }
+ STORED_DATA.each { |k, m| store[k] = send(m) }
yield
ensure
self.controller = nil
@@ -38,12 +40,3 @@ def controller=(value)
end
end
end
-
-ActiveSupport.on_load(:action_controller) do
- if defined?(ActionController::Base)
- ActionController::Base.around_action Audited::Sweeper.new
- end
- if defined?(ActionController::API)
- ActionController::API.around_action Audited::Sweeper.new
- end
-end
diff --git a/lib/audited/version.rb b/lib/audited/version.rb
index c58589f80..4e6222877 100644
--- a/lib/audited/version.rb
+++ b/lib/audited/version.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Audited
- VERSION = "4.9.4"
+ VERSION = "5.8.0"
end
diff --git a/lib/generators/audited/install_generator.rb b/lib/generators/audited/install_generator.rb
index bbc5c49c5..8bf641822 100644
--- a/lib/generators/audited/install_generator.rb
+++ b/lib/generators/audited/install_generator.rb
@@ -1,9 +1,11 @@
-require 'rails/generators'
-require 'rails/generators/migration'
-require 'active_record'
-require 'rails/generators/active_record'
-require 'generators/audited/migration'
-require 'generators/audited/migration_helper'
+# frozen_string_literal: true
+
+require "rails/generators"
+require "rails/generators/migration"
+require "active_record"
+require "rails/generators/active_record"
+require "generators/audited/migration"
+require "generators/audited/migration_helper"
module Audited
module Generators
@@ -18,7 +20,7 @@ class InstallGenerator < Rails::Generators::Base
source_root File.expand_path("../templates", __FILE__)
def copy_migration
- migration_template 'install.rb', 'db/migrate/install_audited.rb'
+ migration_template "install.rb", "db/migrate/install_audited.rb"
end
end
end
diff --git a/lib/generators/audited/migration.rb b/lib/generators/audited/migration.rb
index 600d6de88..26b48d67c 100644
--- a/lib/generators/audited/migration.rb
+++ b/lib/generators/audited/migration.rb
@@ -1,15 +1,25 @@
+# frozen_string_literal: true
+
module Audited
module Generators
module Migration
# Implement the required interface for Rails::Generators::Migration.
- def next_migration_number(dirname) #:nodoc:
+ def next_migration_number(dirname) # :nodoc:
next_migration_number = current_migration_number(dirname) + 1
- if ::ActiveRecord::Base.timestamped_migrations
+ if timestamped_migrations?
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
else
"%.3d" % next_migration_number
end
end
+
+ private
+
+ def timestamped_migrations?
+ (Rails.gem_version >= Gem::Version.new("7.0")) ?
+ ::ActiveRecord.timestamped_migrations :
+ ::ActiveRecord::Base.timestamped_migrations
+ end
end
end
end
diff --git a/lib/generators/audited/migration_helper.rb b/lib/generators/audited/migration_helper.rb
index 37cac7cc1..992461d90 100644
--- a/lib/generators/audited/migration_helper.rb
+++ b/lib/generators/audited/migration_helper.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
module Audited
module Generators
module MigrationHelper
def migration_parent
- Rails::VERSION::MAJOR == 4 ? 'ActiveRecord::Migration' : "ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]"
+ "ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]"
end
end
end
diff --git a/lib/generators/audited/templates/add_association_to_audits.rb b/lib/generators/audited/templates/add_association_to_audits.rb
index b949b4649..d8a601256 100644
--- a/lib/generators/audited/templates/add_association_to_audits.rb
+++ b/lib/generators/audited/templates/add_association_to_audits.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class <%= migration_class_name %> < <%= migration_parent %>
def self.up
add_column :audits, :association_id, :integer
diff --git a/lib/generators/audited/templates/add_comment_to_audits.rb b/lib/generators/audited/templates/add_comment_to_audits.rb
index b20b0abea..35c7a134e 100644
--- a/lib/generators/audited/templates/add_comment_to_audits.rb
+++ b/lib/generators/audited/templates/add_comment_to_audits.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class <%= migration_class_name %> < <%= migration_parent %>
def self.up
add_column :audits, :comment, :string
diff --git a/lib/generators/audited/templates/add_remote_address_to_audits.rb b/lib/generators/audited/templates/add_remote_address_to_audits.rb
index 4cd3f50dc..8be86d526 100644
--- a/lib/generators/audited/templates/add_remote_address_to_audits.rb
+++ b/lib/generators/audited/templates/add_remote_address_to_audits.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class <%= migration_class_name %> < <%= migration_parent %>
def self.up
add_column :audits, :remote_address, :string
diff --git a/lib/generators/audited/templates/add_request_uuid_to_audits.rb b/lib/generators/audited/templates/add_request_uuid_to_audits.rb
index d7c9113fb..41e5b7ea4 100644
--- a/lib/generators/audited/templates/add_request_uuid_to_audits.rb
+++ b/lib/generators/audited/templates/add_request_uuid_to_audits.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class <%= migration_class_name %> < <%= migration_parent %>
def self.up
add_column :audits, :request_uuid, :string
diff --git a/lib/generators/audited/templates/add_version_to_auditable_index.rb b/lib/generators/audited/templates/add_version_to_auditable_index.rb
index 79a4045d6..45d801855 100644
--- a/lib/generators/audited/templates/add_version_to_auditable_index.rb
+++ b/lib/generators/audited/templates/add_version_to_auditable_index.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class <%= migration_class_name %> < <%= migration_parent %>
def self.up
if index_exists?(:audits, [:auditable_type, :auditable_id], name: index_name)
diff --git a/lib/generators/audited/templates/install.rb b/lib/generators/audited/templates/install.rb
index 18f68c395..be5fa30eb 100644
--- a/lib/generators/audited/templates/install.rb
+++ b/lib/generators/audited/templates/install.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class <%= migration_class_name %> < <%= migration_parent %>
def self.up
create_table :audits, :force => true do |t|
diff --git a/lib/generators/audited/templates/rename_association_to_associated.rb b/lib/generators/audited/templates/rename_association_to_associated.rb
index 206e0a5de..3ab5a4fd3 100644
--- a/lib/generators/audited/templates/rename_association_to_associated.rb
+++ b/lib/generators/audited/templates/rename_association_to_associated.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class <%= migration_class_name %> < <%= migration_parent %>
def self.up
if index_exists? :audits, [:association_id, :association_type], :name => 'association_index'
diff --git a/lib/generators/audited/templates/rename_changes_to_audited_changes.rb b/lib/generators/audited/templates/rename_changes_to_audited_changes.rb
index ec7c7b6e1..0e1cace7a 100644
--- a/lib/generators/audited/templates/rename_changes_to_audited_changes.rb
+++ b/lib/generators/audited/templates/rename_changes_to_audited_changes.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class <%= migration_class_name %> < <%= migration_parent %>
def self.up
rename_column :audits, :changes, :audited_changes
diff --git a/lib/generators/audited/templates/rename_parent_to_association.rb b/lib/generators/audited/templates/rename_parent_to_association.rb
index 8c5d63dc1..f279e993c 100644
--- a/lib/generators/audited/templates/rename_parent_to_association.rb
+++ b/lib/generators/audited/templates/rename_parent_to_association.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class <%= migration_class_name %> < <%= migration_parent %>
def self.up
rename_column :audits, :auditable_parent_id, :association_id
diff --git a/lib/generators/audited/templates/revert_polymorphic_indexes_order.rb b/lib/generators/audited/templates/revert_polymorphic_indexes_order.rb
index 1f7a9bbcf..e2b3a0e52 100644
--- a/lib/generators/audited/templates/revert_polymorphic_indexes_order.rb
+++ b/lib/generators/audited/templates/revert_polymorphic_indexes_order.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class <%= migration_class_name %> < <%= migration_parent %>
def self.up
fix_index_order_for [:associated_id, :associated_type], 'associated_index'
diff --git a/lib/generators/audited/upgrade_generator.rb b/lib/generators/audited/upgrade_generator.rb
index d2b019704..b66d082d6 100644
--- a/lib/generators/audited/upgrade_generator.rb
+++ b/lib/generators/audited/upgrade_generator.rb
@@ -1,9 +1,11 @@
-require 'rails/generators'
-require 'rails/generators/migration'
-require 'active_record'
-require 'rails/generators/active_record'
-require 'generators/audited/migration'
-require 'generators/audited/migration_helper'
+# frozen_string_literal: true
+
+require "rails/generators"
+require "rails/generators/migration"
+require "active_record"
+require "rails/generators/active_record"
+require "generators/audited/migration"
+require "generators/audited/migration_helper"
module Audited
module Generators
@@ -27,31 +29,31 @@ def migrations_to_be_applied
columns = Audited::Audit.columns.map(&:name)
indexes = Audited::Audit.connection.indexes(Audited::Audit.table_name)
- yield :add_comment_to_audits unless columns.include?('comment')
+ yield :add_comment_to_audits unless columns.include?("comment")
- if columns.include?('changes')
+ if columns.include?("changes")
yield :rename_changes_to_audited_changes
end
- unless columns.include?('remote_address')
+ unless columns.include?("remote_address")
yield :add_remote_address_to_audits
end
- unless columns.include?('request_uuid')
+ unless columns.include?("request_uuid")
yield :add_request_uuid_to_audits
end
- unless columns.include?('association_id')
- if columns.include?('auditable_parent_id')
+ unless columns.include?("association_id")
+ if columns.include?("auditable_parent_id")
yield :rename_parent_to_association
else
- unless columns.include?('associated_id')
+ unless columns.include?("associated_id")
yield :add_association_to_audits
end
end
end
- if columns.include?('association_id')
+ if columns.include?("association_id")
yield :rename_association_to_associated
end
diff --git a/spec/audited/audit_spec.rb b/spec/audited/audit_spec.rb
index 2f0008557..c18abdb54 100644
--- a/spec/audited/audit_spec.rb
+++ b/spec/audited/audit_spec.rb
@@ -1,6 +1,27 @@
require "spec_helper"
-SingleCov.covered!
+SingleCov.covered! uncovered: 2 # Rails version check
+
+class CustomAudit < Audited::Audit
+ def custom_method
+ "I'm custom!"
+ end
+end
+
+class TempModel1 < ::ActiveRecord::Base
+ self.table_name = :companies
+end
+
+class TempModel2 < ::ActiveRecord::Base
+ self.table_name = :companies
+end
+
+class Models::ActiveRecord::CustomUser < ::ActiveRecord::Base
+end
+
+class Models::ActiveRecord::CustomUserSubclass < Models::ActiveRecord::CustomUser
+ audited
+end
describe Audited::Audit do
let(:user) { Models::ActiveRecord::User.new name: "Testing" }
@@ -9,30 +30,17 @@
around(:example) do |example|
original_audit_class = Audited.audit_class
- class CustomAudit < Audited::Audit
- def custom_method
- "I'm custom!"
- end
- end
-
- class TempModel < ::ActiveRecord::Base
- self.table_name = :companies
- end
-
example.run
Audited.config { |config| config.audit_class = original_audit_class }
- Audited::Audit.audited_class_names.delete("TempModel")
- Object.send(:remove_const, :TempModel)
- Object.send(:remove_const, :CustomAudit)
end
context "when a custom audit class is configured" do
it "should be used in place of #{described_class}" do
- Audited.config { |config| config.audit_class = CustomAudit }
- TempModel.audited
+ Audited.config { |config| config.audit_class = "CustomAudit" }
+ TempModel1.audited
- record = TempModel.create
+ record = TempModel1.create
audit = record.audits.first
expect(audit).to be_a CustomAudit
@@ -42,9 +50,9 @@ class TempModel < ::ActiveRecord::Base
context "when a custom audit class is not configured" do
it "should default to #{described_class}" do
- TempModel.audited
+ TempModel2.audited
- record = TempModel.create
+ record = TempModel2.create
audit = record.audits.first
expect(audit).to be_a Audited::Audit
@@ -62,7 +70,7 @@ class TempModel < ::ActiveRecord::Base
end
it "does not unserialize from binary columns" do
- allow(Audited.audit_class.columns_hash["audited_changes"]).to receive(:type).and_return("foo")
+ allow(Audited::YAMLIfTextColumnType).to receive(:text_column?).and_return(false)
audit.audited_changes = {foo: "bar"}
expect(audit.audited_changes).to eq "{:foo=>\"bar\"}"
end
@@ -72,7 +80,7 @@ class TempModel < ::ActiveRecord::Base
let(:user) { Models::ActiveRecord::User.create(name: "John") }
it "undos changes" do
- user.update_attribute(:name, 'Joe')
+ user.update_attribute(:name, "Joe")
user.audits.last.undo
user.reload
expect(user.name).to eq("John")
@@ -87,12 +95,12 @@ class TempModel < ::ActiveRecord::Base
it "undos creation" do
user # trigger create
- expect {user.audits.last.undo}.to change(Models::ActiveRecord::User, :count).by(-1)
+ expect { user.audits.last.undo }.to change(Models::ActiveRecord::User, :count).by(-1)
end
it "fails when trying to undo unknown" do
audit = user.audits.last
- audit.action = 'oops'
+ audit.action = "oops"
expect { audit.undo }.to raise_error("invalid action given oops")
end
end
@@ -105,8 +113,8 @@ class TempModel < ::ActiveRecord::Base
it "should be able to set the user to nil" do
subject.user_id = 1
- subject.user_type = 'Models::ActiveRecord::User'
- subject.username = 'joe'
+ subject.user_type = "Models::ActiveRecord::User"
+ subject.username = "joe"
subject.user = nil
@@ -117,19 +125,19 @@ class TempModel < ::ActiveRecord::Base
end
it "should be able to set the user to a string" do
- subject.user = 'test'
- expect(subject.user).to eq('test')
+ subject.user = "test"
+ expect(subject.user).to eq("test")
end
it "should clear model when setting to a string" do
subject.user = user
- subject.user = 'testing'
+ subject.user = "testing"
expect(subject.user_id).to be_nil
expect(subject.user_type).to be_nil
end
it "should clear the username when setting to a model" do
- subject.username = 'test'
+ subject.username = "test"
subject.user = user
expect(subject.username).to be_nil
end
@@ -138,7 +146,7 @@ class TempModel < ::ActiveRecord::Base
describe "revision" do
it "should recreate attributes" do
user = Models::ActiveRecord::User.create name: "1"
- 5.times {|i| user.update_attribute :name, (i + 2).to_s }
+ 5.times { |i| user.update_attribute :name, (i + 2).to_s }
user.audits.each do |audit|
expect(audit.revision.name).to eq(audit.version.to_s)
@@ -174,7 +182,7 @@ class TempModel < ::ActiveRecord::Base
it "uses created at" do
Audited::Audit.delete_all
audit = Models::ActiveRecord::User.create(name: "John").audits.last
- audit.update_columns(created_at: Time.parse('2018-01-01'))
+ audit.update_columns(created_at: Time.zone.parse("2018-01-01"))
expect(Audited::Audit.collection_cache_key).to match(/-20180101\d+$/)
end
else
@@ -220,12 +228,6 @@ class TempModel < ::ActiveRecord::Base
end
describe "audited_classes" do
- class Models::ActiveRecord::CustomUser < ::ActiveRecord::Base
- end
- class Models::ActiveRecord::CustomUserSubclass < Models::ActiveRecord::CustomUser
- audited
- end
-
it "should include audited classes" do
expect(Audited::Audit.audited_classes).to include(Models::ActiveRecord::User)
end
@@ -236,17 +238,37 @@ class Models::ActiveRecord::CustomUserSubclass < Models::ActiveRecord::CustomUse
end
describe "new_attributes" do
- it "should return a hash of the new values" do
- new_attributes = Audited::Audit.new(audited_changes: {a: [1, 2], b: [3, 4]}).new_attributes
+ it "should return the audited_changes without modification for create" do
+ new_attributes = Audited::Audit.new(audited_changes: {int: 1, array: [1]}, action: :create).new_attributes
+ expect(new_attributes).to eq({"int" => 1, "array" => [1]})
+ end
+
+ it "should return a hash that contains the after values of each attribute" do
+ new_attributes = Audited::Audit.new(audited_changes: {a: [1, 2], b: [3, 4]}, action: :update).new_attributes
expect(new_attributes).to eq({"a" => 2, "b" => 4})
end
+
+ it "should return the audited_changes without modification for destroy" do
+ new_attributes = Audited::Audit.new(audited_changes: {int: 1, array: [1]}, action: :destroy).new_attributes
+ expect(new_attributes).to eq({"int" => 1, "array" => [1]})
+ end
end
describe "old_attributes" do
- it "should return a hash of the old values" do
- old_attributes = Audited::Audit.new(audited_changes: {a: [1, 2], b: [3, 4]}).old_attributes
+ it "should return the audited_changes without modification for create" do
+ old_attributes = Audited::Audit.new(audited_changes: {int: 1, array: [1]}, action: :create).new_attributes
+ expect(old_attributes).to eq({"int" => 1, "array" => [1]})
+ end
+
+ it "should return a hash that contains the before values of each attribute" do
+ old_attributes = Audited::Audit.new(audited_changes: {a: [1, 2], b: [3, 4]}, action: :update).old_attributes
expect(old_attributes).to eq({"a" => 1, "b" => 3})
end
+
+ it "should return the audited_changes without modification for destroy" do
+ old_attributes = Audited::Audit.new(audited_changes: {int: 1, array: [1]}, action: :destroy).old_attributes
+ expect(old_attributes).to eq({"int" => 1, "array" => [1]})
+ end
end
describe "as_user" do
@@ -293,8 +315,8 @@ class Models::ActiveRecord::CustomUserSubclass < Models::ActiveRecord::CustomUse
end
end
- it "should be thread safe" do
- begin
+ if ActiveRecord::Base.connection.adapter_name != "SQLite"
+ it "should be thread safe" do
expect(user.save).to eq(true)
t1 = Thread.new do
@@ -314,10 +336,10 @@ class Models::ActiveRecord::CustomUserSubclass < Models::ActiveRecord::CustomUse
t1.join
t2.join
end
- end if ActiveRecord::Base.connection.adapter_name != 'SQLite'
+ end
it "should return the value from the yield block" do
- result = Audited::Audit.as_user('foo') do
+ result = Audited::Audit.as_user("foo") do
42
end
expect(result).to eq(42)
@@ -325,10 +347,10 @@ class Models::ActiveRecord::CustomUserSubclass < Models::ActiveRecord::CustomUse
it "should reset audited_user when the yield block raises an exception" do
expect {
- Audited::Audit.as_user('foo') do
- raise StandardError.new('expected')
+ Audited::Audit.as_user("foo") do
+ raise StandardError.new("expected")
end
- }.to raise_exception('expected')
+ }.to raise_exception("expected")
expect(Audited.store[:audited_user]).to be_nil
end
end
diff --git a/spec/audited/auditor_spec.rb b/spec/audited/auditor_spec.rb
index 0753ca15e..4d12feb0b 100644
--- a/spec/audited/auditor_spec.rb
+++ b/spec/audited/auditor_spec.rb
@@ -1,19 +1,83 @@
require "spec_helper"
-SingleCov.covered! uncovered: 13 # not testing proxy_respond_to? hack / 2 methods / deprecation of `version`
+# not testing proxy_respond_to? hack / 2 methods / deprecation of `version`
+# also, an additional 6 around `after_touch` for Versions before 6.
+# Increased to 17/10 to get to green CI as a new baseline, August 2024.
+uncovered = (ActiveRecord::VERSION::MAJOR < 6) ? 17 : 10
+SingleCov.covered! uncovered: uncovered
-describe Audited::Auditor do
+class ConditionalPrivateCompany < ::ActiveRecord::Base
+ self.table_name = "companies"
+
+ audited if: :foo?
+
+ private def foo?
+ true
+ end
+end
+
+class ConditionalCompany < ::ActiveRecord::Base
+ self.table_name = "companies"
+
+ audited if: :public?
+
+ def public?
+ end
+end
+
+class ExclusiveCompany < ::ActiveRecord::Base
+ self.table_name = "companies"
+ audited if: proc { false }
+end
+
+class ExclusionaryCompany < ::ActiveRecord::Base
+ self.table_name = "companies"
+
+ audited unless: :non_profit?
+
+ def non_profit?
+ end
+end
+
+class ExclusionaryCompany2 < ::ActiveRecord::Base
+ self.table_name = "companies"
+ audited unless: proc { |c| c.exclusive? }
+
+ def exclusive?
+ true
+ end
+end
+
+class InclusiveCompany < ::ActiveRecord::Base
+ self.table_name = "companies"
+ audited if: proc { true }
+end
+
+class InclusiveCompany2 < ::ActiveRecord::Base
+ self.table_name = "companies"
+ audited unless: proc { false }
+end
+
+class Secret < ::ActiveRecord::Base
+ audited
+end
+
+class Secret2 < ::ActiveRecord::Base
+ audited
+ self.non_audited_columns = ["delta", "top_secret", "created_at"]
+end
+describe Audited::Auditor do
describe "configuration" do
it "should include instance methods" do
- expect(Models::ActiveRecord::User.new).to be_a_kind_of( Audited::Auditor::AuditedInstanceMethods)
+ expect(Models::ActiveRecord::User.new).to be_a_kind_of(Audited::Auditor::AuditedInstanceMethods)
end
it "should include class methods" do
- expect(Models::ActiveRecord::User).to be_a_kind_of( Audited::Auditor::AuditedClassMethods )
+ expect(Models::ActiveRecord::User).to be_a_kind_of(Audited::Auditor::AuditedClassMethods)
end
- ['created_at', 'updated_at', 'created_on', 'updated_on', 'lock_version', 'id', 'password'].each do |column|
+ ["created_at", "updated_at", "created_on", "updated_on", "lock_version", "id", "password"].each do |column|
it "should not audit #{column}" do
expect(Models::ActiveRecord::User.non_audited_columns).to include(column)
end
@@ -25,64 +89,29 @@
context "when condition method is private" do
subject { ConditionalPrivateCompany.new.send(:auditing_enabled) }
- before do
- class ConditionalPrivateCompany < ::ActiveRecord::Base
- self.table_name = 'companies'
-
- audited if: :foo?
-
- private def foo?
- true
- end
- end
- end
-
it { is_expected.to be_truthy }
end
context "when passing a method name" do
- before do
- class ConditionalCompany < ::ActiveRecord::Base
- self.table_name = 'companies'
-
- audited if: :public?
-
- def public?; end
- end
- end
-
context "when conditions are true" do
before { allow_any_instance_of(ConditionalCompany).to receive(:public?).and_return(true) }
- it { is_expected.to be_truthy }
+ it { is_expected.to be_truthy }
end
context "when conditions are false" do
before { allow_any_instance_of(ConditionalCompany).to receive(:public?).and_return(false) }
- it { is_expected.to be_falsey }
+ it { is_expected.to be_falsey }
end
end
context "when passing a Proc" do
context "when conditions are true" do
- before do
- class InclusiveCompany < ::ActiveRecord::Base
- self.table_name = 'companies'
- audited if: Proc.new { true }
- end
- end
-
subject { InclusiveCompany.new.send(:auditing_enabled) }
it { is_expected.to be_truthy }
end
context "when conditions are false" do
- before do
- class ExclusiveCompany < ::ActiveRecord::Base
- self.table_name = 'companies'
- audited if: Proc.new { false }
- end
- end
subject { ExclusiveCompany.new.send(:auditing_enabled) }
it { is_expected.to be_falsey }
end
@@ -91,67 +120,40 @@ class ExclusiveCompany < ::ActiveRecord::Base
context "should be configurable which conditions aren't audited" do
context "when using a method name" do
- before do
- class ExclusionaryCompany < ::ActiveRecord::Base
- self.table_name = 'companies'
-
- audited unless: :non_profit?
-
- def non_profit?; end
- end
- end
-
subject { ExclusionaryCompany.new.send(:auditing_enabled) }
context "when conditions are true" do
before { allow_any_instance_of(ExclusionaryCompany).to receive(:non_profit?).and_return(true) }
- it { is_expected.to be_falsey }
+ it { is_expected.to be_falsey }
end
context "when conditions are false" do
before { allow_any_instance_of(ExclusionaryCompany).to receive(:non_profit?).and_return(false) }
- it { is_expected.to be_truthy }
+ it { is_expected.to be_truthy }
end
end
context "when using a proc" do
context "when conditions are true" do
- before do
- class ExclusionaryCompany < ::ActiveRecord::Base
- self.table_name = 'companies'
- audited unless: Proc.new { |c| c.exclusive? }
-
- def exclusive?
- true
- end
- end
- end
-
- subject { ExclusionaryCompany.new.send(:auditing_enabled) }
- it { is_expected.to be_falsey }
+ subject { ExclusionaryCompany2.new.send(:auditing_enabled) }
+ it { is_expected.to be_falsey }
end
context "when conditions are false" do
- before do
- class InclusiveCompany < ::ActiveRecord::Base
- self.table_name = 'companies'
- audited unless: Proc.new { false }
- end
- end
-
- subject { InclusiveCompany.new.send(:auditing_enabled) }
- it { is_expected.to be_truthy }
+ subject { InclusiveCompany2.new.send(:auditing_enabled) }
+ it { is_expected.to be_truthy }
end
end
end
it "should be configurable which attributes are not audited via ignored_attributes" do
- Audited.ignored_attributes = ['delta', 'top_secret', 'created_at']
- class Secret < ::ActiveRecord::Base
- audited
- end
+ Audited.ignored_attributes = ["delta", "top_secret", "created_at", "updated_at"]
+
+ expect(Secret.non_audited_columns).to include("delta", "top_secret", "created_at")
+ end
- expect(Secret.non_audited_columns).to include('delta', 'top_secret', 'created_at')
+ it "should be configurable which attributes are not audited via non_audited_columns=" do
+ expect(Secret2.non_audited_columns).to include("delta", "top_secret", "created_at")
end
it "should be configurable which attributes are not audited via non_audited_columns=" do
@@ -168,7 +170,7 @@ class Secret2 < ::ActiveRecord::Base
begin
Models::ActiveRecord::User.non_audited_columns += [:favourite_device]
- expect(create_user.audits.first.audited_changes.keys.any? { |col| ['favourite_device', 'created_at', 'updated_at', 'password'].include?( col ) }).to eq(false)
+ expect(create_user.audits.first.audited_changes.keys.any? { |col| ["favourite_device", "created_at", "updated_at", "password"].include?(col) }).to eq(false)
ensure
Models::ActiveRecord::User.non_audited_columns = previous
end
@@ -190,7 +192,7 @@ def non_column_attr=(val)
user.password = "password"
user.non_column_attr = "some value"
user.save!
- expect(user.audits.last.audited_changes.keys).to eq(%w{password})
+ expect(user.audits.last.audited_changes.keys).to eq(%w[password])
end
it "should save attributes not specified in 'except' option" do
@@ -209,10 +211,80 @@ def non_column_attr=(val)
user.password = "password"
user.non_column_attr = "some value"
user.save!
- expect(user.audits.last.audited_changes.keys).to eq(%w{non_column_attr})
+ expect(user.audits.last.audited_changes.keys).to eq(%w[non_column_attr])
+ end
+
+ it "should redact columns specified in 'redacted' option" do
+ redacted = Audited::Auditor::AuditedInstanceMethods::REDACTED
+ user = Models::ActiveRecord::UserRedactedPassword.create(password: "password")
+ user.save!
+ expect(user.audits.last.audited_changes["password"]).to eq(redacted)
+ user.password = "new_password"
+ user.save!
+ expect(user.audits.last.audited_changes["password"]).to eq([redacted, redacted])
+ end
+
+ it "should redact columns specified in 'redacted' option when there are multiple specified" do
+ redacted = Audited::Auditor::AuditedInstanceMethods::REDACTED
+ user =
+ Models::ActiveRecord::UserMultipleRedactedAttributes.create(
+ password: "password"
+ )
+ user.save!
+ expect(user.audits.last.audited_changes["password"]).to eq(redacted)
+ # Saving '[REDACTED]' value for 'ssn' even if value wasn't set explicitly when record was created
+ expect(user.audits.last.audited_changes["ssn"]).to eq(redacted)
+
+ user.password = "new_password"
+ user.ssn = 987654321
+ user.save!
+ expect(user.audits.last.audited_changes["password"]).to eq([redacted, redacted])
+ expect(user.audits.last.audited_changes["ssn"]).to eq([redacted, redacted])
+
+ # If we haven't changed any attrs from 'redacted' list, audit should not contain these keys
+ user.name = "new name"
+ user.save!
+ expect(user.audits.last.audited_changes).to have_key("name")
+ expect(user.audits.last.audited_changes).not_to have_key("password")
+ expect(user.audits.last.audited_changes).not_to have_key("ssn")
+ end
+
+ it "should redact columns in 'redacted' column with custom option" do
+ user = Models::ActiveRecord::UserRedactedPasswordCustomRedaction.create(password: "password")
+ user.save!
+ expect(user.audits.last.audited_changes["password"]).to eq(["My", "Custom", "Value", 7])
+ end
+
+ context "when ignored_default_callbacks is set" do
+ before { Audited.ignored_default_callbacks = [:create] }
+ after { Audited.ignored_default_callbacks = [] }
+
+ it "should remove create callback" do
+ class DefaultCallback < ::ActiveRecord::Base
+ audited
+ end
+
+ expect(DefaultCallback.audited_options[:on]).to eq([:update, :touch, :destroy])
+ end
+
+ it "should keep create callback if specified" do
+ class CallbacksSpecified < ::ActiveRecord::Base
+ audited on: [:create, :update, :destroy]
+ end
+
+ expect(CallbacksSpecified.audited_options[:on]).to eq([:create, :update, :destroy])
+ end
+ end
+
+ if ::ActiveRecord::VERSION::MAJOR >= 7
+ it "should filter encrypted attributes" do
+ user = Models::ActiveRecord::UserWithEncryptedPassword.create(password: "password")
+ user.save
+ expect(user.audits.last.audited_changes["password"]).to eq("[FILTERED]")
+ end
end
- if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
+ if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
describe "'json' and 'jsonb' audited_changes column type" do
let(:migrations_path) { SPEC_ROOT.join("support/active_record/postgres") }
@@ -249,16 +321,16 @@ def non_column_attr=(val)
it "should allow mass assignment of all unprotected attributes" do
yesterday = 1.day.ago
- u = Models::ActiveRecord::NoAttributeProtectionUser.new(name: 'name',
- username: 'username',
- password: 'password',
- activated: true,
- suspended_at: yesterday,
- logins: 2)
+ u = Models::ActiveRecord::NoAttributeProtectionUser.new(name: "name",
+ username: "username",
+ password: "password",
+ activated: true,
+ suspended_at: yesterday,
+ logins: 2)
- expect(u.name).to eq('name')
- expect(u.username).to eq('username')
- expect(u.password).to eq('password')
+ expect(u.name).to eq("name")
+ expect(u.username).to eq("username")
+ expect(u.password).to eq("password")
expect(u.activated).to eq(true)
expect(u.suspended_at.to_i).to eq(yesterday.to_i)
expect(u.logins).to eq(2)
@@ -266,12 +338,12 @@ def non_column_attr=(val)
end
describe "on create" do
- let( :user ) { create_user status: :reliable, audit_comment: "Create" }
+ let(:user) { create_user status: :reliable, audit_comment: "Create" }
it "should change the audit count" do
expect {
user
- }.to change( Audited::Audit, :count ).by(1)
+ }.to change(Audited::Audit, :count).by(1)
end
it "should create associated audit" do
@@ -279,7 +351,7 @@ def non_column_attr=(val)
end
it "should set the action to create" do
- expect(user.audits.first.action).to eq('create')
+ expect(user.audits.first.action).to eq("create")
expect(Audited::Audit.creates.order(:id).last).to eq(user.audits.first)
expect(user.audits.creates.count).to eq(1)
expect(user.audits.updates.count).to eq(0)
@@ -294,46 +366,66 @@ def non_column_attr=(val)
expect(user.audits.first.audited_changes["status"]).to eq(1)
end
+ context "when store_synthesized_enums is set to true" do
+ before { Audited.store_synthesized_enums = true }
+ after { Audited.store_synthesized_enums = false }
+
+ it "should store enum value as Rails synthesized value" do
+ expect(user.audits.first.audited_changes["status"]).to eq("reliable")
+ end
+ end
+
it "should store comment" do
- expect(user.audits.first.comment).to eq('Create')
+ expect(user.audits.first.comment).to eq("Create")
end
it "should not audit an attribute which is excepted if specified on create or destroy" do
- on_create_destroy_except_name = Models::ActiveRecord::OnCreateDestroyExceptName.create(name: 'Bart')
- expect(on_create_destroy_except_name.audits.first.audited_changes.keys.any?{|col| ['name'].include? col}).to eq(false)
+ on_create_destroy_except_name = Models::ActiveRecord::OnCreateDestroyExceptName.create(name: "Bart")
+ expect(on_create_destroy_except_name.audits.first.audited_changes.keys.any? { |col| ["name"].include? col }).to eq(false)
end
it "should not save an audit if only specified on update/destroy" do
expect {
- Models::ActiveRecord::OnUpdateDestroy.create!( name: 'Bart' )
- }.to_not change( Audited::Audit, :count )
+ Models::ActiveRecord::OnUpdateDestroy.create!(name: "Bart")
+ }.to_not change(Audited::Audit, :count)
+ end
+
+ it "should save readonly columns" do
+ expect {
+ Models::ActiveRecord::UserWithReadOnlyAttrs.create!(name: "Bart")
+ }.to change(Audited::Audit, :count)
end
end
describe "on update" do
before do
- @user = create_user( name: 'Brandon', status: :active, audit_comment: 'Update' )
+ @user = create_user(name: "Brandon", status: :active, audit_comment: "Update")
end
it "should save an audit" do
expect {
@user.update_attribute(:name, "Someone")
- }.to change( Audited::Audit, :count ).by(1)
+ }.to change(Audited::Audit, :count).by(1)
expect {
@user.update_attribute(:name, "Someone else")
- }.to change( Audited::Audit, :count ).by(1)
+ }.to change(Audited::Audit, :count).by(1)
end
it "should set the action to 'update'" do
- @user.update! name: 'Changed'
- expect(@user.audits.last.action).to eq('update')
+ @user.update! name: "Changed"
+ expect(@user.audits.last.action).to eq("update")
expect(Audited::Audit.updates.order(:id).last).to eq(@user.audits.last)
expect(@user.audits.updates.last).to eq(@user.audits.last)
end
it "should store the changed attributes" do
- @user.update! name: 'Changed'
- expect(@user.audits.last.audited_changes).to eq({ 'name' => ['Brandon', 'Changed'] })
+ @user.update! name: "Changed"
+ expect(@user.audits.last.audited_changes).to eq({"name" => ["Brandon", "Changed"]})
+ end
+
+ it "should store changed enum values" do
+ @user.update! status: 1
+ expect(@user.audits.last.audited_changes["status"]).to eq([0, 1])
end
it "should store changed enum values" do
@@ -342,35 +434,146 @@ def non_column_attr=(val)
end
it "should store audit comment" do
- expect(@user.audits.last.comment).to eq('Update')
+ expect(@user.audits.last.comment).to eq("Update")
end
it "should not save an audit if only specified on create/destroy" do
- on_create_destroy = Models::ActiveRecord::OnCreateDestroy.create( name: 'Bart' )
+ on_create_destroy = Models::ActiveRecord::OnCreateDestroy.create(name: "Bart")
expect {
- on_create_destroy.update! name: 'Changed'
- }.to_not change( Audited::Audit, :count )
+ on_create_destroy.update! name: "Changed"
+ }.to_not change(Audited::Audit, :count)
end
it "should not save an audit if the value doesn't change after type casting" do
@user.update! logins: 0, activated: true
- expect { @user.update_attribute :logins, '0' }.to_not change( Audited::Audit, :count )
- expect { @user.update_attribute :activated, 1 }.to_not change( Audited::Audit, :count )
- expect { @user.update_attribute :activated, '1' }.to_not change( Audited::Audit, :count )
+ expect { @user.update_attribute :logins, "0" }.to_not change(Audited::Audit, :count)
+ expect { @user.update_attribute :activated, 1 }.to_not change(Audited::Audit, :count)
+ expect { @user.update_attribute :activated, "1" }.to_not change(Audited::Audit, :count)
+ end
+
+ context "with readonly attributes" do
+ before do
+ @user = create_user_with_readonly_attrs(status: "active")
+ end
+
+ it "should not save readonly columns" do
+ expect { @user.update! status: "banned" }.to_not change(Audited::Audit, :count)
+ end
end
describe "with no dirty changes" do
it "does not create an audit if the record is not changed" do
expect {
@user.save!
- }.to_not change( Audited::Audit, :count )
+ }.to_not change(Audited::Audit, :count)
end
it "creates an audit when an audit comment is present" do
expect {
@user.audit_comment = "Comment"
@user.save!
- }.to change( Audited::Audit, :count )
+ }.to change(Audited::Audit, :count)
+ end
+ end
+ end
+
+ if ::ActiveRecord::VERSION::MAJOR >= 6
+ describe "on touch" do
+ before do
+ @user = create_user(name: "Brandon", status: :active)
+ end
+
+ it "should save an audit" do
+ expect { @user.touch(:suspended_at) }.to change(Audited::Audit, :count).by(1)
+ end
+
+ it "should set the action to 'update'" do
+ @user.touch(:suspended_at)
+ expect(@user.audits.last.action).to eq("update")
+ expect(Audited::Audit.updates.order(:id).last).to eq(@user.audits.last)
+ expect(@user.audits.updates.last).to eq(@user.audits.last)
+ end
+
+ it "should store the changed attributes" do
+ @user.touch(:suspended_at)
+ expect(@user.audits.last.audited_changes["suspended_at"][0]).to be_nil
+ expect(Time.parse(@user.audits.last.audited_changes["suspended_at"][1].to_s)).to be_within(2.seconds).of(Time.current)
+ end
+
+ it "should store audit comment" do
+ @user.audit_comment = "Here exists a touch comment"
+ @user.touch(:suspended_at)
+ expect(@user.audits.last.action).to eq("update")
+ expect(@user.audits.last.comment).to eq("Here exists a touch comment")
+ end
+
+ it "should not save an audit if only specified on create/destroy" do
+ on_create_destroy = Models::ActiveRecord::OnCreateDestroyUser.create(name: "Bart")
+ expect {
+ on_create_destroy.touch(:suspended_at)
+ }.to_not change(Audited::Audit, :count)
+ end
+
+ it "should store an audit if touch is the only audit" do
+ on_touch = Models::ActiveRecord::OnTouchOnly.create(name: "Bart")
+ expect {
+ on_touch.update(name: "NotBart")
+ }.to_not change(Audited::Audit, :count)
+ expect {
+ on_touch.touch(:suspended_at)
+ }.to change(on_touch.audits, :count).from(0).to(1)
+
+ @user.audits.destroy_all
+ expect(@user.audits).to be_empty
+ expect {
+ @user.touch(:suspended_at)
+ }.to change(@user.audits, :count).from(0).to(1)
+ end
+
+ context "don't double audit" do
+ let(:user) { Models::ActiveRecord::Owner.create(name: "OwnerUser", suspended_at: 1.month.ago, companies_attributes: [{name: "OwnedCompany"}]) }
+ let(:company) { user.companies.first }
+
+ it "should only create 1 (create) audit for object" do
+ expect(user.audits.count).to eq(1)
+ expect(user.audits.first.action).to eq("create")
+ end
+
+ it "should only create 1 (create) audit for nested resource" do
+ expect(company.audits.count).to eq(1)
+ expect(company.audits.first.action).to eq("create")
+ end
+
+ context "after creating" do
+ it "updating / touching nested resource shouldn't save touch audit on parent object" do
+ expect { company.touch(:type) }.not_to change(user.audits, :count)
+ expect { company.update(type: "test") }.not_to change(user.audits, :count)
+ end
+
+ it "updating / touching parent object shouldn't save previous data" do
+ expect { user.touch(:suspended_at) }.to change(user.audits, :count).from(1).to(2)
+ expect(user.audits.last.action).to eq("update")
+ expect(user.audits.last.audited_changes.keys).to eq(%w[suspended_at])
+ end
+
+ it "updating nested resource through parent while changing an enum on parent shouldn't double audit" do
+ user.status = :reliable
+ user.companies_attributes = [{name: "test"}]
+ expect { user.save }.to change(user.audits, :count).from(1).to(2)
+ expect(user.audits.last.action).to eq("update")
+ expect(user.audits.last.audited_changes.keys).to eq(%w[status])
+ end
+ end
+
+ context "after updating" do
+ it "changing nested resource shouldn't audit owner" do
+ expect { user.update(username: "test") }.to change(user.audits, :count).from(1).to(2)
+ expect { company.update(type: "test") }.not_to change(user.audits, :count)
+
+ expect { user.touch(:suspended_at) }.to change(user.audits, :count).from(2).to(3)
+ expect { company.update(type: "another_test") }.not_to change(user.audits, :count)
+ end
+ end
end
end
end
@@ -383,7 +586,7 @@ def non_column_attr=(val)
it "should save an audit" do
expect {
@user.destroy
- }.to change( Audited::Audit, :count )
+ }.to change(Audited::Audit, :count)
expect(@user.audits.size).to eq(2)
end
@@ -391,7 +594,7 @@ def non_column_attr=(val)
it "should set the action to 'destroy'" do
@user.destroy
- expect(@user.audits.last.action).to eq('destroy')
+ expect(@user.audits.last.action).to eq("destroy")
expect(Audited::Audit.destroys.order(:id).last).to eq(@user.audits.last)
expect(@user.audits.destroys.last).to eq(@user.audits.last)
end
@@ -416,11 +619,11 @@ def non_column_attr=(val)
end
it "should not save an audit if only specified on create/update" do
- on_create_update = Models::ActiveRecord::OnCreateUpdate.create!( name: 'Bart' )
+ on_create_update = Models::ActiveRecord::OnCreateUpdate.create!(name: "Bart")
expect {
on_create_update.destroy
- }.to_not change( Audited::Audit, :count )
+ }.to_not change(Audited::Audit, :count)
end
it "should audit dependent destructions" do
@@ -429,9 +632,9 @@ def non_column_attr=(val)
expect {
owner.destroy
- }.to change( Audited::Audit, :count )
+ }.to change(Audited::Audit, :count)
- expect(company.audits.map { |a| a.action }).to eq(['create', 'destroy'])
+ expect(company.audits.map { |a| a.action }).to eq(["create", "destroy"])
end
end
@@ -443,20 +646,20 @@ def non_column_attr=(val)
user.destroy
}.to_not raise_error
- expect( user.audits ).to be_empty
+ expect(user.audits).to be_empty
end
end
describe "associated with" do
- let(:owner) { Models::ActiveRecord::Owner.create(name: 'Models::ActiveRecord::Owner') }
- let(:owned_company) { Models::ActiveRecord::OwnedCompany.create!(name: 'The auditors', owner: owner) }
+ let(:owner) { Models::ActiveRecord::Owner.create(name: "Models::ActiveRecord::Owner") }
+ let(:owned_company) { Models::ActiveRecord::OwnedCompany.create!(name: "The auditors", owner: owner) }
it "should record the associated object on create" do
expect(owned_company.audits.first.associated).to eq(owner)
end
it "should store the associated object on update" do
- owned_company.update_attribute(:name, 'The Auditors')
+ owned_company.update_attribute(:name, "The Auditors")
expect(owned_company.audits.last.associated).to eq(owner)
end
@@ -467,8 +670,8 @@ def non_column_attr=(val)
end
describe "has associated audits" do
- let!(:owner) { Models::ActiveRecord::Owner.create!(name: 'Models::ActiveRecord::Owner') }
- let!(:owned_company) { Models::ActiveRecord::OwnedCompany.create!(name: 'The auditors', owner: owner) }
+ let!(:owner) { Models::ActiveRecord::Owner.create!(name: "Models::ActiveRecord::Owner") }
+ let!(:owned_company) { Models::ActiveRecord::OwnedCompany.create!(name: "The auditors", owner: owner) }
it "should list the associated audits" do
expect(owner.associated_audits.length).to eq(1)
@@ -492,7 +695,7 @@ def non_column_attr=(val)
it "should delete old audits when keeped amount exceeded" do
stub_global_max_audits(2) do
user = create_versions(2)
- user.update(name: 'John')
+ user.update(name: "John")
expect(user.audits.pluck(:version)).to eq([2, 3])
end
end
@@ -500,35 +703,35 @@ def non_column_attr=(val)
it "should not delete old audits when keeped amount not exceeded" do
stub_global_max_audits(3) do
user = create_versions(2)
- user.update(name: 'John')
+ user.update(name: "John")
expect(user.audits.pluck(:version)).to eq([1, 2, 3])
end
end
it "should delete old extra audits after introducing limit" do
stub_global_max_audits(nil) do
- user = Models::ActiveRecord::User.create!(name: 'Brandon', username: 'brandon')
- user.update!(name: 'Foobar')
- user.update!(name: 'Awesome', username: 'keepers')
+ user = Models::ActiveRecord::User.create!(name: "Brandon", username: "brandon")
+ user.update!(name: "Foobar")
+ user.update!(name: "Awesome", username: "keepers")
user.update!(activated: true)
Audited.max_audits = 3
Models::ActiveRecord::User.send(:normalize_audited_options)
- user.update!(favourite_device: 'Android Phone')
+ user.update!(favourite_device: "Android Phone")
audits = user.audits
expect(audits.count).to eq(3)
- expect(audits[0].audited_changes).to include({'name' => ['Foobar', 'Awesome'], 'username' => ['brandon', 'keepers']})
- expect(audits[1].audited_changes).to eq({'activated' => [nil, true]})
- expect(audits[2].audited_changes).to eq({'favourite_device' => [nil, 'Android Phone']})
+ expect(audits[0].audited_changes).to include({"name" => ["Foobar", "Awesome"], "username" => ["brandon", "keepers"]})
+ expect(audits[1].audited_changes).to eq({"activated" => [nil, true]})
+ expect(audits[2].audited_changes).to eq({"favourite_device" => [nil, "Android Phone"]})
end
end
it "should add comment line for combined audit" do
stub_global_max_audits(2) do
- user = Models::ActiveRecord::User.create!(name: 'Foobar 1')
- user.update(name: 'Foobar 2', audit_comment: 'First audit comment')
- user.update(name: 'Foobar 3', audit_comment: 'Second audit comment')
+ user = Models::ActiveRecord::User.create!(name: "Foobar 1")
+ user.update(name: "Foobar 2", audit_comment: "First audit comment")
+ user.update(name: "Foobar 3", audit_comment: "Second audit comment")
expect(user.audits.first.comment).to match(/First audit comment.+is the result of multiple/m)
end
end
@@ -548,10 +751,10 @@ def stub_global_max_audits(max_audits)
end
describe "revisions" do
- let( :user ) { create_versions }
+ let(:user) { create_versions }
it "should return an Array of Users" do
- expect(user.revisions).to be_a_kind_of( Array )
+ expect(user.revisions).to be_a_kind_of(Array)
user.revisions.each { |version| expect(version).to be_a_kind_of Models::ActiveRecord::User }
end
@@ -560,38 +763,38 @@ def stub_global_max_audits(max_audits)
end
it "should have one revision for each audit" do
- expect(user.audits.size).to eql( user.revisions.size )
+ expect(user.audits.size).to eql(user.revisions.size)
end
it "should set the attributes for each revision" do
- u = Models::ActiveRecord::User.create(name: 'Brandon', username: 'brandon')
- u.update! name: 'Foobar'
- u.update! name: 'Awesome', username: 'keepers'
+ u = Models::ActiveRecord::User.create(name: "Brandon", username: "brandon")
+ u.update! name: "Foobar"
+ u.update! name: "Awesome", username: "keepers"
expect(u.revisions.size).to eql(3)
- expect(u.revisions[0].name).to eql('Brandon')
- expect(u.revisions[0].username).to eql('brandon')
+ expect(u.revisions[0].name).to eql("Brandon")
+ expect(u.revisions[0].username).to eql("brandon")
- expect(u.revisions[1].name).to eql('Foobar')
- expect(u.revisions[1].username).to eql('brandon')
+ expect(u.revisions[1].name).to eql("Foobar")
+ expect(u.revisions[1].username).to eql("brandon")
- expect(u.revisions[2].name).to eql('Awesome')
- expect(u.revisions[2].username).to eql('keepers')
+ expect(u.revisions[2].name).to eql("Awesome")
+ expect(u.revisions[2].username).to eql("keepers")
end
it "access to only recent revisions" do
- u = Models::ActiveRecord::User.create(name: 'Brandon', username: 'brandon')
- u.update! name: 'Foobar'
- u.update! name: 'Awesome', username: 'keepers'
+ u = Models::ActiveRecord::User.create(name: "Brandon", username: "brandon")
+ u.update! name: "Foobar"
+ u.update! name: "Awesome", username: "keepers"
expect(u.revisions(2).size).to eq(2)
- expect(u.revisions(2)[0].name).to eq('Foobar')
- expect(u.revisions(2)[0].username).to eq('brandon')
+ expect(u.revisions(2)[0].name).to eq("Foobar")
+ expect(u.revisions(2)[0].username).to eq("brandon")
- expect(u.revisions(2)[1].name).to eq('Awesome')
- expect(u.revisions(2)[1].username).to eq('keepers')
+ expect(u.revisions(2)[1].name).to eq("Awesome")
+ expect(u.revisions(2)[1].username).to eq("keepers")
end
it "should be empty if no audits exist" do
@@ -600,13 +803,13 @@ def stub_global_max_audits(max_audits)
end
it "should ignore attributes that have been deleted" do
- user.audits.last.update! audited_changes: {old_attribute: 'old value'}
+ user.audits.last.update! audited_changes: {old_attribute: "old value"}
expect { user.revisions }.to_not raise_error
end
end
describe "revisions" do
- let( :user ) { create_versions(5) }
+ let(:user) { create_versions(5) }
it "should maintain identity" do
expect(user.revision(1)).to eq(user)
@@ -614,15 +817,15 @@ def stub_global_max_audits(max_audits)
it "should find the given revision" do
revision = user.revision(3)
- expect(revision).to be_a_kind_of( Models::ActiveRecord::User )
+ expect(revision).to be_a_kind_of(Models::ActiveRecord::User)
expect(revision.audit_version).to eq(3)
- expect(revision.name).to eq('Foobar 3')
+ expect(revision.name).to eq("Foobar 3")
end
it "should find the previous revision with :previous" do
revision = user.revision(:previous)
expect(revision.audit_version).to eq(4)
- #expect(revision).to eq(user.revision(4))
+ # expect(revision).to eq(user.revision(4))
expect(revision.attributes).to eq(user.revision(4).attributes)
end
@@ -633,7 +836,7 @@ def stub_global_max_audits(max_audits)
end
it "should be able to set protected attributes" do
- u = Models::ActiveRecord::User.create(name: 'Brandon')
+ u = Models::ActiveRecord::User.create(name: "Brandon")
u.update_attribute :logins, 1
u.update_attribute :logins, 2
@@ -643,23 +846,33 @@ def stub_global_max_audits(max_audits)
end
it "should set attributes directly" do
- u = Models::ActiveRecord::User.create(name: '')
- expect(u.revision(1).name).to eq('<Joe>')
+ u = Models::ActiveRecord::User.create(name: "")
+ expect(u.revision(1).name).to eq("<Joe>")
end
it "should set the attributes for each revision" do
- u = Models::ActiveRecord::User.create(name: 'Brandon', username: 'brandon')
- u.update! name: 'Foobar'
- u.update! name: 'Awesome', username: 'keepers'
+ u = Models::ActiveRecord::User.create(name: "Brandon", username: "brandon")
+ u.update! name: "Foobar"
+ u.update! name: "Awesome", username: "keepers"
+
+ expect(u.revision(3).name).to eq("Awesome")
+ expect(u.revision(3).username).to eq("keepers")
- expect(u.revision(3).name).to eq('Awesome')
- expect(u.revision(3).username).to eq('keepers')
+ expect(u.revision(2).name).to eq("Foobar")
+ expect(u.revision(2).username).to eq("brandon")
- expect(u.revision(2).name).to eq('Foobar')
- expect(u.revision(2).username).to eq('brandon')
+ expect(u.revision(1).name).to eq("Brandon")
+ expect(u.revision(1).username).to eq("brandon")
+ end
- expect(u.revision(1).name).to eq('Brandon')
- expect(u.revision(1).username).to eq('brandon')
+ it "should correctly restore revision with enum" do
+ u = Models::ActiveRecord::User.create(status: :active)
+ u.update_attribute(:status, :reliable)
+ u.update_attribute(:status, :banned)
+
+ expect(u.revision(3)).to be_banned
+ expect(u.revision(2)).to be_reliable
+ expect(u.revision(1)).to be_active
end
it "should correctly restore revision with enum" do
@@ -690,14 +903,26 @@ def stub_global_max_audits(max_audits)
it "should record new audit when saving revision" do
expect {
user.revision(1).save!
- }.to change( user.audits, :count ).by(1)
+ }.to change(user.audits, :count).by(1)
end
it "should re-insert destroyed records" do
user.destroy
expect {
user.revision(1).save!
- }.to change( Models::ActiveRecord::User, :count ).by(1)
+ }.to change(Models::ActiveRecord::User, :count).by(1)
+ end
+
+ it "should return nil for values greater than the number of revisions" do
+ expect(user.revision(user.revisions.count + 1)).to be_nil
+ end
+
+ it "should work with array attributes" do
+ u = Models::ActiveRecord::User.create!(phone_numbers: ["+1 800-444-4444"])
+ u.update!(phone_numbers: ["+1 804-222-1111", "+1 317 222-2222"])
+
+ expect(u.revision(0).phone_numbers).to eq(["+1 804-222-1111", "+1 317 222-2222"])
+ expect(u.revision(1).phone_numbers).to eq(["+1 800-444-4444"])
end
it "should return nil for values greater than the number of revisions" do
@@ -706,18 +931,58 @@ def stub_global_max_audits(max_audits)
end
describe "revision_at" do
- let( :user ) { create_user }
+ let(:user) { create_user }
it "should find the latest revision before the given time" do
audit = user.audits.first
audit.created_at = 1.hour.ago
audit.save!
- user.update! name: 'updated'
- expect(user.revision_at( 2.minutes.ago ).audit_version).to eq(1)
+ user.update! name: "updated"
+ expect(user.revision_at(2.minutes.ago).audit_version).to eq(1)
end
it "should be nil if given a time before audits" do
- expect(user.revision_at( 1.week.ago )).to be_nil
+ expect(user.revision_at(1.week.ago)).to be_nil
+ end
+ end
+
+ describe "own_and_associated_audits" do
+ it "should return audits for self and associated audits" do
+ owner = Models::ActiveRecord::Owner.create!
+ company = owner.companies.create!
+ company.update!(name: "Collective Idea")
+
+ other_owner = Models::ActiveRecord::Owner.create!
+ other_owner.companies.create!
+
+ expect(owner.own_and_associated_audits).to match_array(owner.audits + company.audits)
+ end
+
+ it "should return audits for STI classes" do
+ # Where parent is STI
+ sti_company = Models::ActiveRecord::Company::STICompany.create!
+ sti_company.update!(name: "Collective Idea")
+ expect(sti_company.own_and_associated_audits).to match_array(sti_company.audits)
+
+ # Where associated is STI
+ owner = Models::ActiveRecord::Owner.create!
+ company = owner.companies.create! type: "Models::ActiveRecord::OwnedCompany::STICompany"
+ company.update!(name: "Collective Idea")
+ expect(owner.own_and_associated_audits).to match_array(owner.audits + company.audits)
+ end
+
+ it "should order audits by creation time" do
+ owner = Models::ActiveRecord::Owner.create!
+ first_audit = owner.audits.first
+ first_audit.update_column(:created_at, 1.year.ago)
+
+ company = owner.companies.create!
+ second_audit = company.audits.first
+ second_audit.update_column(:created_at, 1.month.ago)
+
+ company.update!(name: "Collective Idea")
+ third_audit = company.audits.last
+ expect(owner.own_and_associated_audits.to_a).to eq([third_audit, second_audit, first_audit])
end
end
@@ -751,19 +1016,32 @@ def stub_global_max_audits(max_audits)
describe "without auditing" do
it "should not save an audit when calling #save_without_auditing" do
expect {
- u = Models::ActiveRecord::User.new(name: 'Brandon')
+ u = Models::ActiveRecord::User.new(name: "Brandon")
expect(u.save_without_auditing).to eq(true)
- }.to_not change( Audited::Audit, :count )
+ }.to_not change(Audited::Audit, :count)
end
it "should not save an audit inside of the #without_auditing block" do
expect {
- Models::ActiveRecord::User.without_auditing { Models::ActiveRecord::User.create!( name: 'Brandon' ) }
- }.to_not change( Audited::Audit, :count )
+ Models::ActiveRecord::User.without_auditing { Models::ActiveRecord::User.create!(name: "Brandon") }
+ }.to_not change(Audited::Audit, :count)
+ end
+
+ context "when global audits are disabled" do
+ it "should re-enable class audits after #without_auditing block" do
+ Audited.auditing_enabled = false
+ Models::ActiveRecord::User.without_auditing {}
+ Audited.auditing_enabled = true
+ expect(Models::ActiveRecord::User.auditing_enabled).to eql(true)
+ end
end
it "should reset auditing status even it raises an exception" do
- Models::ActiveRecord::User.without_auditing { raise } rescue nil
+ begin
+ Models::ActiveRecord::User.without_auditing { raise }
+ rescue
+ nil
+ end
expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
end
@@ -774,7 +1052,7 @@ def stub_global_max_audits(max_audits)
expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
Models::ActiveRecord::User.without_auditing do
expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
- Models::ActiveRecord::User.create!( name: 'Bart' )
+ Models::ActiveRecord::User.create!(name: "Bart")
sleep 1
expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
end
@@ -784,13 +1062,13 @@ def stub_global_max_audits(max_audits)
t2 = Thread.new do
sleep 0.5
expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
- Models::ActiveRecord::User.create!( name: 'Lisa' )
+ Models::ActiveRecord::User.create!(name: "Lisa")
end
t1.join
t2.join
- expect(Models::ActiveRecord::User.find_by_name('Bart').audits.count).to eq(0)
- expect(Models::ActiveRecord::User.find_by_name('Lisa').audits.count).to eq(1)
+ expect(Models::ActiveRecord::User.find_by_name("Bart").audits.count).to eq(0)
+ expect(Models::ActiveRecord::User.find_by_name("Lisa").audits.count).to eq(1)
end
it "should not save an audit when auditing is globally disabled" do
@@ -804,7 +1082,7 @@ def stub_global_max_audits(max_audits)
Audited.auditing_enabled = true
expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
- user.update!(name: 'Test')
+ user.update!(name: "Test")
expect(user.audits.count).to eq(1)
Models::ActiveRecord::User.enable_auditing
end
@@ -813,24 +1091,37 @@ def stub_global_max_audits(max_audits)
describe "with auditing" do
it "should save an audit when calling #save_with_auditing" do
expect {
- u = Models::ActiveRecord::User.new(name: 'Brandon')
+ u = Models::ActiveRecord::User.new(name: "Brandon")
Models::ActiveRecord::User.auditing_enabled = false
expect(u.save_with_auditing).to eq(true)
Models::ActiveRecord::User.auditing_enabled = true
- }.to change( Audited::Audit, :count ).by(1)
+ }.to change(Audited::Audit, :count).by(1)
end
it "should save an audit inside of the #with_auditing block" do
expect {
Models::ActiveRecord::User.auditing_enabled = false
- Models::ActiveRecord::User.with_auditing { Models::ActiveRecord::User.create!( name: 'Brandon' ) }
+ Models::ActiveRecord::User.with_auditing { Models::ActiveRecord::User.create!(name: "Brandon") }
Models::ActiveRecord::User.auditing_enabled = true
- }.to change( Audited::Audit, :count ).by(1)
+ }.to change(Audited::Audit, :count).by(1)
+ end
+
+ context "when global audits are disabled" do
+ it "should re-enable class audits after #with_auditing block" do
+ Audited.auditing_enabled = false
+ Models::ActiveRecord::User.with_auditing {}
+ Audited.auditing_enabled = true
+ expect(Models::ActiveRecord::User.auditing_enabled).to eql(true)
+ end
end
it "should reset auditing status even it raises an exception" do
Models::ActiveRecord::User.disable_auditing
- Models::ActiveRecord::User.with_auditing { raise } rescue nil
+ begin
+ Models::ActiveRecord::User.with_auditing { raise }
+ rescue
+ nil
+ end
expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
Models::ActiveRecord::User.enable_auditing
end
@@ -844,7 +1135,7 @@ def stub_global_max_audits(max_audits)
Models::ActiveRecord::User.with_auditing do
expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
- Models::ActiveRecord::User.create!( name: 'Shaggy' )
+ Models::ActiveRecord::User.create!(name: "Shaggy")
sleep 1
expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
end
@@ -856,74 +1147,81 @@ def stub_global_max_audits(max_audits)
sleep 0.5
Models::ActiveRecord::User.disable_auditing
expect(Models::ActiveRecord::User.auditing_enabled).to eq(false)
- Models::ActiveRecord::User.create!( name: 'Scooby' )
+ Models::ActiveRecord::User.create!(name: "Scooby")
Models::ActiveRecord::User.enable_auditing
end
t1.join
t2.join
Models::ActiveRecord::User.enable_auditing
- expect(Models::ActiveRecord::User.find_by_name('Shaggy').audits.count).to eq(1)
- expect(Models::ActiveRecord::User.find_by_name('Scooby').audits.count).to eq(0)
+ expect(Models::ActiveRecord::User.find_by_name("Shaggy").audits.count).to eq(1)
+ expect(Models::ActiveRecord::User.find_by_name("Scooby").audits.count).to eq(0)
end
end
describe "comment required" do
-
describe "on create" do
it "should not validate when audit_comment is not supplied when initialized" do
- expect(Models::ActiveRecord::CommentRequiredUser.new(name: 'Foo')).not_to be_valid
+ expect(Models::ActiveRecord::CommentRequiredUser.new(name: "Foo")).not_to be_valid
end
it "should not validate when audit_comment is not supplied trying to create" do
- expect(Models::ActiveRecord::CommentRequiredUser.create(name: 'Foo')).not_to be_valid
+ expect(Models::ActiveRecord::CommentRequiredUser.create(name: "Foo")).not_to be_valid
end
it "should validate when audit_comment is supplied" do
- expect(Models::ActiveRecord::CommentRequiredUser.create(name: 'Foo', audit_comment: 'Create')).to be_valid
+ expect(Models::ActiveRecord::CommentRequiredUser.create(name: "Foo", audit_comment: "Create")).to be_valid
end
it "should validate when audit_comment is not supplied, and creating is not being audited" do
- expect(Models::ActiveRecord::OnUpdateCommentRequiredUser.create(name: 'Foo')).to be_valid
- expect(Models::ActiveRecord::OnDestroyCommentRequiredUser.create(name: 'Foo')).to be_valid
+ expect(Models::ActiveRecord::OnUpdateCommentRequiredUser.create(name: "Foo")).to be_valid
+ expect(Models::ActiveRecord::OnDestroyCommentRequiredUser.create(name: "Foo")).to be_valid
end
it "should validate when audit_comment is not supplied, and auditing is disabled" do
Models::ActiveRecord::CommentRequiredUser.disable_auditing
- expect(Models::ActiveRecord::CommentRequiredUser.create(name: 'Foo')).to be_valid
+ expect(Models::ActiveRecord::CommentRequiredUser.create(name: "Foo")).to be_valid
Models::ActiveRecord::CommentRequiredUser.enable_auditing
end
+
+ it "should validate when audit_comment is not supplied, and only excluded attributes changed" do
+ expect(Models::ActiveRecord::CommentRequiredUser.new(password: "Foo")).to be_valid
+ end
end
describe "on update" do
- let( :user ) { Models::ActiveRecord::CommentRequiredUser.create!( audit_comment: 'Create' ) }
- let( :on_create_user ) { Models::ActiveRecord::OnDestroyCommentRequiredUser.create }
- let( :on_destroy_user ) { Models::ActiveRecord::OnDestroyCommentRequiredUser.create }
+ let(:user) { Models::ActiveRecord::CommentRequiredUser.create!(audit_comment: "Create") }
+ let(:on_create_user) { Models::ActiveRecord::OnDestroyCommentRequiredUser.create }
+ let(:on_destroy_user) { Models::ActiveRecord::OnDestroyCommentRequiredUser.create }
it "should not validate when audit_comment is not supplied" do
- expect(user.update(name: 'Test')).to eq(false)
+ expect(user.update(name: "Test")).to eq(false)
end
it "should validate when audit_comment is not supplied, and updating is not being audited" do
- expect(on_create_user.update(name: 'Test')).to eq(true)
- expect(on_destroy_user.update(name: 'Test')).to eq(true)
+ expect(on_create_user.update(name: "Test")).to eq(true)
+ expect(on_destroy_user.update(name: "Test")).to eq(true)
end
it "should validate when audit_comment is supplied" do
- expect(user.update(name: 'Test', audit_comment: 'Update')).to eq(true)
+ expect(user.update(name: "Test", audit_comment: "Update")).to eq(true)
end
it "should validate when audit_comment is not supplied, and auditing is disabled" do
Models::ActiveRecord::CommentRequiredUser.disable_auditing
- expect(user.update(name: 'Test')).to eq(true)
+ expect(user.update(name: "Test")).to eq(true)
Models::ActiveRecord::CommentRequiredUser.enable_auditing
end
+
+ it "should validate when audit_comment is not supplied, and only excluded attributes changed" do
+ expect(user.update(password: "Test")).to eq(true)
+ end
end
describe "on destroy" do
- let( :user ) { Models::ActiveRecord::CommentRequiredUser.create!( audit_comment: 'Create' )}
- let( :on_create_user ) { Models::ActiveRecord::OnCreateCommentRequiredUser.create!( audit_comment: 'Create' ) }
- let( :on_update_user ) { Models::ActiveRecord::OnUpdateCommentRequiredUser.create }
+ let(:user) { Models::ActiveRecord::CommentRequiredUser.create!(audit_comment: "Create") }
+ let(:on_create_user) { Models::ActiveRecord::OnCreateCommentRequiredUser.create!(audit_comment: "Create") }
+ let(:on_update_user) { Models::ActiveRecord::OnUpdateCommentRequiredUser.create }
it "should not validate when audit_comment is not supplied" do
expect(user.destroy).to eq(false)
@@ -945,7 +1243,15 @@ def stub_global_max_audits(max_audits)
Models::ActiveRecord::CommentRequiredUser.enable_auditing
end
end
+ end
+
+ describe "no update with comment only" do
+ let(:user) { Models::ActiveRecord::NoUpdateWithCommentOnlyUser.create }
+ it "does not create an audit when only an audit_comment is present" do
+ user.audit_comment = "Comment"
+ expect { user.save! }.to_not change(Audited::Audit, :count)
+ end
end
describe "no update with comment only" do
@@ -959,27 +1265,26 @@ def stub_global_max_audits(max_audits)
end
describe "attr_protected and attr_accessible" do
-
it "should not raise error when attr_accessible is set and protected is false" do
expect {
- Models::ActiveRecord::AccessibleAfterDeclarationUser.new(name: 'No fail!')
+ Models::ActiveRecord::AccessibleAfterDeclarationUser.new(name: "No fail!")
}.to_not raise_error
end
it "should not rause an error when attr_accessible is declared before audited" do
expect {
- Models::ActiveRecord::AccessibleAfterDeclarationUser.new(name: 'No fail!')
+ Models::ActiveRecord::AccessibleAfterDeclarationUser.new(name: "No fail!")
}.to_not raise_error
end
end
describe "audit_as" do
- let( :user ) { Models::ActiveRecord::User.create name: 'Testing' }
+ let(:user) { Models::ActiveRecord::User.create name: "Testing" }
it "should record user objects" do
- Models::ActiveRecord::Company.audit_as( user ) do
- company = Models::ActiveRecord::Company.create name: 'The auditors'
- company.update! name: 'The Auditors'
+ Models::ActiveRecord::Company.audit_as(user) do
+ company = Models::ActiveRecord::Company.create name: "The auditors"
+ company.update! name: "The Auditors"
company.audits.each do |audit|
expect(audit.user).to eq(user)
@@ -988,9 +1293,9 @@ def stub_global_max_audits(max_audits)
end
it "should record usernames" do
- Models::ActiveRecord::Company.audit_as( user.name ) do
- company = Models::ActiveRecord::Company.create name: 'The auditors'
- company.update! name: 'The Auditors'
+ Models::ActiveRecord::Company.audit_as(user.name) do
+ company = Models::ActiveRecord::Company.create name: "The auditors"
+ company.update! name: "The Auditors"
company.audits.each do |audit|
expect(audit.user).to eq(user.name)
@@ -1000,7 +1305,7 @@ def stub_global_max_audits(max_audits)
end
describe "after_audit" do
- let( :user ) { Models::ActiveRecord::UserWithAfterAudit.new }
+ let(:user) { Models::ActiveRecord::UserWithAfterAudit.new }
it "should invoke after_audit callback on create" do
expect(user.bogus_attr).to be_nil
@@ -1010,7 +1315,7 @@ def stub_global_max_audits(max_audits)
end
describe "around_audit" do
- let( :user ) { Models::ActiveRecord::UserWithAfterAudit.new }
+ let(:user) { Models::ActiveRecord::UserWithAfterAudit.new }
it "should invoke around_audit callback on create" do
expect(user.around_attr).to be_nil
@@ -1021,13 +1326,29 @@ def stub_global_max_audits(max_audits)
describe "STI auditing" do
it "should correctly disable auditing when using STI" do
- company = Models::ActiveRecord::Company::STICompany.create name: 'The auditors'
+ company = Models::ActiveRecord::Company::STICompany.create name: "The auditors"
expect(company.type).to eq("Models::ActiveRecord::Company::STICompany")
expect {
Models::ActiveRecord::Company.auditing_enabled = false
- company.update! name: 'STI auditors'
+ company.update! name: "STI auditors"
Models::ActiveRecord::Company.auditing_enabled = true
- }.to_not change( Audited::Audit, :count )
+ }.to_not change(Audited::Audit, :count)
+ end
+ end
+
+ describe "call audit multiple times" do
+ it "should update audit options" do
+ user = Models::ActiveRecord::UserOnlyName.create
+ user.update(password: "new password 1", name: "new name 1")
+ expect(user.audits.last.audited_changes.keys).to eq(%w[name])
+
+ user.class.class_eval do
+ audited only: :password
+ end
+
+ user = Models::ActiveRecord::UserOnlyName.last
+ user.update(password: "new password 2", name: "new name 2")
+ expect(user.audits.last.audited_changes.keys).to eq(%w[password])
end
end
end
diff --git a/spec/audited/sweeper_spec.rb b/spec/audited/sweeper_spec.rb
index bb850c158..3ffb56cec 100644
--- a/spec/audited/sweeper_spec.rb
+++ b/spec/audited/sweeper_spec.rb
@@ -1,6 +1,6 @@
require "spec_helper"
-SingleCov.covered! uncovered: 2 # 2 conditional on_load conditions
+SingleCov.covered!
class AuditsController < ActionController::Base
before_action :populate_user
@@ -13,7 +13,7 @@ def create
end
def update
- current_user.update!(password: 'foo')
+ current_user.update!(password: "foo")
head :ok
end
@@ -22,7 +22,8 @@ def update
attr_accessor :current_user
attr_accessor :custom_user
- def populate_user; end
+ def populate_user
+ end
end
describe AuditsController do
@@ -30,6 +31,7 @@ def populate_user; end
render_views
before do
+ Audited::Railtie.initializers.each(&:run)
Audited.current_user_method = :current_user
end
@@ -40,7 +42,7 @@ def populate_user; end
controller.send(:current_user=, user)
expect {
post :create
- }.to change( Audited::Audit, :count )
+ }.to change(Audited::Audit, :count)
expect(controller.company.audits.last.user).to eq(user)
end
@@ -50,7 +52,7 @@ def populate_user; end
Audited.current_user_method = :nope
expect {
post :create
- }.to change( Audited::Audit, :count )
+ }.to change(Audited::Audit, :count)
expect(controller.company.audits.last.user).to eq(nil)
end
@@ -60,18 +62,18 @@ def populate_user; end
expect {
post :create
- }.to change( Audited::Audit, :count )
+ }.to change(Audited::Audit, :count)
expect(controller.company.audits.last.user).to eq(user)
end
it "should record the remote address responsible for the change" do
- request.env['REMOTE_ADDR'] = "1.2.3.4"
+ request.env["REMOTE_ADDR"] = "1.2.3.4"
controller.send(:current_user=, user)
post :create
- expect(controller.company.audits.last.remote_address).to eq('1.2.3.4')
+ expect(controller.company.audits.last.remote_address).to eq("1.2.3.4")
end
it "should record a UUID for the web request responsible for the change" do
@@ -113,32 +115,31 @@ def populate_user; end
controller.send(:current_user=, user)
expect {
- put :update, Rails::VERSION::MAJOR == 4 ? {id: 123} : {params: {id: 123}}
- }.to_not change( Audited::Audit, :count )
+ put :update, params: {id: 123}
+ }.to_not change(Audited::Audit, :count)
end
end
end
describe Audited::Sweeper do
-
it "should be thread-safe" do
instance = Audited::Sweeper.new
t1 = Thread.new do
sleep 0.5
- instance.controller = 'thread1 controller instance'
- expect(instance.controller).to eq('thread1 controller instance')
+ instance.controller = "thread1 controller instance"
+ expect(instance.controller).to eq("thread1 controller instance")
end
t2 = Thread.new do
- instance.controller = 'thread2 controller instance'
+ instance.controller = "thread2 controller instance"
sleep 1
- expect(instance.controller).to eq('thread2 controller instance')
+ expect(instance.controller).to eq("thread2 controller instance")
end
- t1.join; t2.join
+ t1.join
+ t2.join
expect(instance.controller).to be_nil
end
-
end
diff --git a/spec/audited_spec.rb b/spec/audited_spec.rb
new file mode 100644
index 000000000..eeaecd780
--- /dev/null
+++ b/spec/audited_spec.rb
@@ -0,0 +1,21 @@
+require "spec_helper"
+
+describe Audited do
+ describe "#store" do
+ describe "maintains state of store" do
+ let(:current_user) { Models::ActiveRecord::User.new(name: 'Some User', username: 'some_username') }
+
+ it "can store and retrieve current_user" do
+ expect(Audited.store[:current_user]).to be_nil
+
+ Audited.store[:current_user] = current_user
+
+ expect(Audited.store[:current_user]).to eq(current_user)
+ end
+
+ it "checks store is not nil" do
+ expect(Audited.store).not_to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/audited_spec_helpers.rb b/spec/audited_spec_helpers.rb
index 105c5fbf1..06faba71b 100644
--- a/spec/audited_spec_helpers.rb
+++ b/spec/audited_spec_helpers.rb
@@ -1,15 +1,18 @@
module AuditedSpecHelpers
-
def create_user(attrs = {})
- Models::ActiveRecord::User.create({name: 'Brandon', username: 'brandon', password: 'password', favourite_device: 'Android Phone'}.merge(attrs))
+ Models::ActiveRecord::User.create({name: "Brandon", username: "brandon", password: "password", favourite_device: "Android Phone"}.merge(attrs))
+ end
+
+ def create_user_with_readonly_attrs(attrs = {})
+ Models::ActiveRecord::UserWithReadOnlyAttrs.create({name: "Brandon", username: "brandon", password: "password", favourite_device: "Android Phone"}.merge(attrs))
end
def build_user(attrs = {})
- Models::ActiveRecord::User.new({name: 'darth', username: 'darth', password: 'noooooooo'}.merge(attrs))
+ Models::ActiveRecord::User.new({name: "darth", username: "darth", password: "noooooooo"}.merge(attrs))
end
def create_versions(n = 2, attrs = {})
- Models::ActiveRecord::User.create(name: 'Foobar 1', **attrs).tap do |u|
+ Models::ActiveRecord::User.create(name: "Foobar 1", **attrs).tap do |u|
(n - 1).times do |i|
u.update_attribute :name, "Foobar #{i + 2}"
end
@@ -18,10 +21,12 @@ def create_versions(n = 2, attrs = {})
end
def run_migrations(direction, migrations_paths, target_version = nil)
- if rails_below?('5.2.0.rc1')
+ if rails_below?("5.2.0.rc1")
ActiveRecord::Migrator.send(direction, migrations_paths, target_version)
- else
+ elsif rails_below?("6.0.0.rc1") || rails_at_least?("7.2.0")
ActiveRecord::MigrationContext.new(migrations_paths).send(direction, target_version)
+ else
+ ActiveRecord::MigrationContext.new(migrations_paths, ActiveRecord::SchemaMigration).send(direction, target_version)
end
end
@@ -29,4 +34,7 @@ def rails_below?(rails_version)
Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new(rails_version)
end
+ def rails_at_least?(rails_version)
+ Gem::Version.new(Rails::VERSION::STRING) >= Gem::Version.new(rails_version)
+ end
end
diff --git a/spec/rails_app/app/assets/config/manifest.js b/spec/rails_app/app/assets/config/manifest.js
new file mode 100644
index 000000000..fa3b57485
--- /dev/null
+++ b/spec/rails_app/app/assets/config/manifest.js
@@ -0,0 +1,2 @@
+//= link application.js
+//= link application.css
diff --git a/spec/rails_app/config/application.rb b/spec/rails_app/config/application.rb
index 6b6bbd24f..a485f848b 100644
--- a/spec/rails_app/config/application.rb
+++ b/spec/rails_app/config/application.rb
@@ -1,13 +1,46 @@
-require 'rails/all'
+require "active_record/railtie"
module RailsApp
class Application < Rails::Application
- config.root = File.expand_path('../../', __FILE__)
+ config.root = File.expand_path("../../", __FILE__)
config.i18n.enforce_available_locales = true
+
+ if Rails.gem_version >= Gem::Version.new("7.1") && config.active_record.respond_to?(:yaml_column_permitted_classes=)
+ config.active_record.yaml_column_permitted_classes = [
+ String,
+ Symbol,
+ Integer,
+ NilClass,
+ Float,
+ Time,
+ Date,
+ FalseClass,
+ Hash,
+ Array,
+ DateTime,
+ TrueClass,
+ BigDecimal,
+ ActiveSupport::TimeWithZone,
+ ActiveSupport::TimeZone,
+ ActiveSupport::HashWithIndifferentAccess
+ ]
+ elsif !Rails.version.start_with?("5.0") && !Rails.version.start_with?("5.1") && config.active_record.respond_to?(:yaml_column_permitted_classes=)
+ config.active_record.yaml_column_permitted_classes =
+ %w[String Symbol Integer NilClass Float Time Date FalseClass Hash Array DateTime TrueClass BigDecimal
+ ActiveSupport::TimeWithZone ActiveSupport::TimeZone ActiveSupport::HashWithIndifferentAccess]
+ end
+
+ if Rails.gem_version >= Gem::Version.new("7.1")
+ config.active_support.cache_format_version = 7.1
+ end
+
+ if Rails.gem_version >= Gem::Version.new("8.0.0.alpha")
+ config.active_support.to_time_preserves_timezone = :zone
+ end
end
end
-require 'active_record/connection_adapters/sqlite3_adapter'
+require "active_record/connection_adapters/sqlite3_adapter"
if ActiveRecord::ConnectionAdapters::SQLite3Adapter.respond_to?(:represent_boolean_as_integer)
ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = true
end
diff --git a/spec/rails_app/config/database.yml b/spec/rails_app/config/database.yml
index 0b0ad3394..fe6bc5f1b 100644
--- a/spec/rails_app/config/database.yml
+++ b/spec/rails_app/config/database.yml
@@ -9,7 +9,8 @@ sqlite3: &SQLITE
postgresql: &POSTGRES
adapter: postgresql
username: postgres
- password:
+ password: postgres
+ host: localhost
database: audited_test
min_messages: ERROR
@@ -17,9 +18,9 @@ mysql: &MYSQL
adapter: mysql2
host: localhost
username: root
- password:
+ password: root
database: audited_test
charset: utf8
-gemtest:
+test:
<<: *<%= ENV['DB'] || 'SQLITE3MEM' %>
diff --git a/spec/rails_app/config/environment.rb b/spec/rails_app/config/environment.rb
index cb86aabf1..cfd0f9b37 100644
--- a/spec/rails_app/config/environment.rb
+++ b/spec/rails_app/config/environment.rb
@@ -1,5 +1,5 @@
# Load the rails application
-require File.expand_path('../application', __FILE__)
+require File.expand_path("../application", __FILE__)
# Initialize the rails application
RailsApp::Application.initialize!
diff --git a/spec/rails_app/config/environments/development.rb b/spec/rails_app/config/environments/development.rb
deleted file mode 100644
index f9c62c393..000000000
--- a/spec/rails_app/config/environments/development.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-RailsApp::Application.configure do
- # Settings specified here will take precedence over those in config/environment.rb
-
- # In the development environment your application's code is reloaded on
- # every request. This slows down response time but is perfect for development
- # since you don't have to restart the webserver when you make code changes.
- config.cache_classes = false
-
- # Log error messages when you accidentally call methods on nil.
- # config.whiny_nils = true
-
- # Show full error reports and disable caching
- config.consider_all_requests_local = true
- config.action_view.debug_rjs = true
- config.action_controller.perform_caching = false
-
- # Don't care if the mailer can't send
- config.action_mailer.raise_delivery_errors = false
-
- config.eager_load = false
-end
diff --git a/spec/rails_app/config/environments/production.rb b/spec/rails_app/config/environments/production.rb
deleted file mode 100644
index 44b5e8aba..000000000
--- a/spec/rails_app/config/environments/production.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-RailsApp::Application.configure do
- # Settings specified here will take precedence over those in config/environment.rb
-
- # The production environment is meant for finished, "live" apps.
- # Code is not reloaded between requests
- config.cache_classes = true
-
- # Full error reports are disabled and caching is turned on
- config.consider_all_requests_local = false
- config.action_controller.perform_caching = true
-
- # See everything in the log (default is :info)
- # config.log_level = :debug
-
- # Use a different logger for distributed setups
- # config.logger = SyslogLogger.new
-
- # Use a different cache store in production
- # config.cache_store = :mem_cache_store
-
- # Disable Rails's static asset server
- # In production, Apache or nginx will already do this
- config.serve_static_assets = false
-
- # Enable serving of images, stylesheets, and javascripts from an asset server
- # config.action_controller.asset_host = "http://assets.example.com"
-
- # Disable delivery errors, bad email addresses will be ignored
- # config.action_mailer.raise_delivery_errors = false
-
- # Enable threaded mode
- # config.threadsafe!
-
- config.eager_load = true
-end
diff --git a/spec/rails_app/config/environments/gemtest.rb b/spec/rails_app/config/environments/test.rb
similarity index 78%
rename from spec/rails_app/config/environments/gemtest.rb
rename to spec/rails_app/config/environments/test.rb
index a10e08fac..78d3403cd 100644
--- a/spec/rails_app/config/environments/gemtest.rb
+++ b/spec/rails_app/config/environments/test.rb
@@ -15,14 +15,14 @@
# Configure static file server for tests with Cache-Control for performance.
if config.respond_to?(:public_file_server)
config.public_file_server.enabled = true
- config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' }
+ config.public_file_server.headers = {"Cache-Control" => "public, max-age=3600"}
else
- config.static_cache_control = 'public, max-age=3600'
- config.serve_static_files = true
+ config.static_cache_control = "public, max-age=3600"
+ config.serve_static_files = true
end
# Show full error reports and disable caching.
- config.consider_all_requests_local = true
+ config.consider_all_requests_local = true
# config.action_controller.perform_caching = false
# Raise exceptions instead of rendering exception templates.
@@ -34,7 +34,7 @@
# Tell Action Mailer not to deliver emails to the real world.
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
- config.action_mailer.delivery_method = :test
+ # config.action_mailer.delivery_method = :test
# Randomize the order test cases are executed.
config.active_support.test_order = :random
@@ -44,4 +44,9 @@
# Raises error for missing translations
# config.action_view.raise_on_missing_translations = true
+
+ if ::ActiveRecord::VERSION::MAJOR >= 7
+ config.active_record.encryption.key_derivation_salt = SecureRandom.hex
+ config.active_record.encryption.primary_key = SecureRandom.hex
+ end
end
diff --git a/spec/rails_app/config/initializers/secret_token.rb b/spec/rails_app/config/initializers/secret_token.rb
index b58b44627..4971db6ef 100644
--- a/spec/rails_app/config/initializers/secret_token.rb
+++ b/spec/rails_app/config/initializers/secret_token.rb
@@ -1,3 +1,3 @@
-Rails.application.config.secret_token = 'ea942c41850d502f2c8283e26bdc57829f471bb18224ddff0a192c4f32cdf6cb5aa0d82b3a7a7adbeb640c4b06f3aa1cd5f098162d8240f669b39d6b49680571'
+Rails.application.config.secret_token = "ea942c41850d502f2c8283e26bdc57829f471bb18224ddff0a192c4f32cdf6cb5aa0d82b3a7a7adbeb640c4b06f3aa1cd5f098162d8240f669b39d6b49680571"
Rails.application.config.session_store :cookie_store, key: "_my_app"
-Rails.application.config.secret_key_base = 'secret value'
+Rails.application.config.secret_key_base = "secret value"
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 7af535c00..306d40fbb 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,24 +1,28 @@
-ENV['RAILS_ENV'] = 'gemtest'
-require 'bundler/setup'
-require 'single_cov'
+ENV["RAILS_ENV"] = "test"
+require "bundler/setup"
+require "single_cov"
SingleCov.setup :rspec
-if Bundler.definition.dependencies.map(&:name).include?('protected_attributes')
- require 'protected_attributes'
+if Bundler.definition.dependencies.map(&:name).include?("protected_attributes")
+ require "protected_attributes"
end
-require 'rails_app/config/environment'
-require 'rspec/rails'
-require 'audited'
-require 'audited-rspec'
-require 'audited_spec_helpers'
-require 'support/active_record/models'
+require "rails_app/config/environment"
+require "rspec/rails"
+require "audited"
+require "audited-rspec"
+require "audited_spec_helpers"
+require "support/active_record/models"
-SPEC_ROOT = Pathname.new(File.expand_path('../', __FILE__))
+SPEC_ROOT = Pathname.new(File.expand_path("../", __FILE__))
-Dir[SPEC_ROOT.join('support/*.rb')].each{|f| require f }
+Dir[SPEC_ROOT.join("support/*.rb")].sort.each { |f| require f }
RSpec.configure do |config|
config.include AuditedSpecHelpers
- config.use_transactional_fixtures = false if Rails.version.start_with?('4.')
+ config.before do
+ allow_any_instance_of(ActiveSupport::StringInquirer).to receive(:test?).and_return(false)
+ end
+ config.use_transactional_fixtures = false if Rails.version.start_with?("4.")
config.use_transactional_tests = false if config.respond_to?(:use_transactional_tests=)
end
+
diff --git a/spec/support/active_record/models.rb b/spec/support/active_record/models.rb
index fe013a639..5a5c4d7a1 100644
--- a/spec/support/active_record/models.rb
+++ b/spec/support/active_record/models.rb
@@ -1,13 +1,24 @@
-require 'cgi'
-require File.expand_path('../schema', __FILE__)
+require "cgi"
+require File.expand_path("../schema", __FILE__)
module Models
module ActiveRecord
class User < ::ActiveRecord::Base
audited except: :password
- attribute :non_column_attr if Rails.version >= '5.1'
+ attribute :non_column_attr if Rails.gem_version >= Gem::Version.new("5.1")
attr_protected :logins if respond_to?(:attr_protected)
- enum status: { active: 0, reliable: 1, banned: 2 }
+
+ if Rails.gem_version >= Gem::Version.new("7.2")
+ enum :status, {active: 0, reliable: 1, banned: 2}
+ else
+ enum status: {active: 0, reliable: 1, banned: 2}
+ end
+
+ if Rails.gem_version >= Gem::Version.new("7.1")
+ serialize :phone_numbers, type: Array
+ else
+ serialize :phone_numbers, Array
+ end
def name=(val)
write_attribute(:name, CGI.escapeHTML(val))
@@ -21,13 +32,68 @@ class UserExceptPassword < ::ActiveRecord::Base
class UserOnlyPassword < ::ActiveRecord::Base
self.table_name = :users
- attribute :non_column_attr if Rails.version >= '5.1'
+ attribute :non_column_attr if Rails.gem_version >= Gem::Version.new("5.1")
audited only: :password
end
+ class UserOnlyName < ::ActiveRecord::Base
+ self.table_name = :users
+ attribute :non_column_attr if Rails.gem_version >= Gem::Version.new("5.1")
+ audited only: :name
+ end
+
+ class UserRedactedPassword < ::ActiveRecord::Base
+ self.table_name = :users
+ audited redacted: :password
+ end
+
+ class UserMultipleRedactedAttributes < ::ActiveRecord::Base
+ self.table_name = :users
+ audited redacted: [:password, :ssn]
+ end
+
+ class UserRedactedPasswordCustomRedaction < ::ActiveRecord::Base
+ self.table_name = :users
+ audited redacted: :password, redaction_value: ["My", "Custom", "Value", 7]
+ end
+
+ if ::ActiveRecord::VERSION::MAJOR >= 7
+ class UserWithEncryptedPassword < ::ActiveRecord::Base
+ self.table_name = :users
+ audited
+ encrypts :password
+ end
+ end
+
+ class UserWithReadOnlyAttrs < ::ActiveRecord::Base
+ self.table_name = :users
+ audited
+ attr_readonly :status
+ end
+
class CommentRequiredUser < ::ActiveRecord::Base
self.table_name = :users
- audited comment_required: true
+ audited except: :password, comment_required: true
+ end
+
+ class OnCreateCommentRequiredUser < ::ActiveRecord::Base
+ self.table_name = :users
+ audited comment_required: true, on: :create
+ end
+
+ class OnUpdateCommentRequiredUser < ::ActiveRecord::Base
+ self.table_name = :users
+ audited comment_required: true, on: :update
+ end
+
+ class OnDestroyCommentRequiredUser < ::ActiveRecord::Base
+ self.table_name = :users
+ audited comment_required: true, on: :destroy
+ end
+
+ class NoUpdateWithCommentOnlyUser < ::ActiveRecord::Base
+ self.table_name = :users
+ audited update_with_comment_only: false
end
class OnCreateCommentRequiredUser < ::ActiveRecord::Base
@@ -96,37 +162,57 @@ class Company::STICompany < Company
end
class Owner < ::ActiveRecord::Base
- self.table_name = 'users'
+ self.table_name = "users"
audited
has_associated_audits
has_many :companies, class_name: "OwnedCompany", dependent: :destroy
+ accepts_nested_attributes_for :companies
+
+ if Rails.gem_version >= Gem::Version.new("7.2")
+ enum :status, {active: 0, reliable: 1, banned: 2}
+ else
+ enum status: {active: 0, reliable: 1, banned: 2}
+ end
end
class OwnedCompany < ::ActiveRecord::Base
- self.table_name = 'companies'
- belongs_to :owner, class_name: "Owner"
+ self.table_name = "companies"
+ belongs_to :owner, class_name: "Owner", touch: true
attr_accessible :name, :owner if respond_to?(:attr_accessible) # declare attr_accessible before calling aaa
audited associated_with: :owner
end
+ class OwnedCompany::STICompany < OwnedCompany
+ end
+
class OnUpdateDestroy < ::ActiveRecord::Base
- self.table_name = 'companies'
+ self.table_name = "companies"
audited on: [:update, :destroy]
end
class OnCreateDestroy < ::ActiveRecord::Base
- self.table_name = 'companies'
+ self.table_name = "companies"
+ audited on: [:create, :destroy]
+ end
+
+ class OnCreateDestroyUser < ::ActiveRecord::Base
+ self.table_name = "users"
audited on: [:create, :destroy]
end
class OnCreateDestroyExceptName < ::ActiveRecord::Base
- self.table_name = 'companies'
+ self.table_name = "companies"
audited except: :name, on: [:create, :destroy]
end
class OnCreateUpdate < ::ActiveRecord::Base
- self.table_name = 'companies'
+ self.table_name = "companies"
audited on: [:create, :update]
end
+
+ class OnTouchOnly < ::ActiveRecord::Base
+ self.table_name = "users"
+ audited on: [:touch]
+ end
end
end
diff --git a/spec/support/active_record/postgres/1_change_audited_changes_type_to_json.rb b/spec/support/active_record/postgres/1_change_audited_changes_type_to_json.rb
index 26af16a30..16600c148 100644
--- a/spec/support/active_record/postgres/1_change_audited_changes_type_to_json.rb
+++ b/spec/support/active_record/postgres/1_change_audited_changes_type_to_json.rb
@@ -1,5 +1,4 @@
-parent = Rails::VERSION::MAJOR == 4 ? ActiveRecord::Migration : ActiveRecord::Migration[4.2]
-class ChangeAuditedChangesTypeToJson < parent
+class ChangeAuditedChangesTypeToJson < ActiveRecord::Migration[5.0]
def self.up
remove_column :audits, :audited_changes
add_column :audits, :audited_changes, :json
diff --git a/spec/support/active_record/postgres/2_change_audited_changes_type_to_jsonb.rb b/spec/support/active_record/postgres/2_change_audited_changes_type_to_jsonb.rb
index 711e100ac..7924ba3c3 100644
--- a/spec/support/active_record/postgres/2_change_audited_changes_type_to_jsonb.rb
+++ b/spec/support/active_record/postgres/2_change_audited_changes_type_to_jsonb.rb
@@ -1,5 +1,4 @@
-parent = Rails::VERSION::MAJOR == 4 ? ActiveRecord::Migration : ActiveRecord::Migration[4.2]
-class ChangeAuditedChangesTypeToJsonb < parent
+class ChangeAuditedChangesTypeToJsonb < ActiveRecord::Migration[5.0]
def self.up
remove_column :audits, :audited_changes
add_column :audits, :audited_changes, :jsonb
diff --git a/spec/support/active_record/schema.rb b/spec/support/active_record/schema.rb
index a710f1bc5..7a1ba9aa5 100644
--- a/spec/support/active_record/schema.rb
+++ b/spec/support/active_record/schema.rb
@@ -1,28 +1,33 @@
-require 'active_record'
-require 'logger'
+require "active_record"
+require "logger"
begin
- db_config = ActiveRecord::Base.configurations[Rails.env].clone
- db_type = db_config['adapter']
- db_name = db_config.delete('database')
- raise Exception.new('No database name specified.') if db_name.blank?
- if db_type == 'sqlite3'
- db_file = Pathname.new(__FILE__).dirname.join(db_name)
- db_file.unlink if db_file.file?
+ if ActiveRecord.version >= Gem::Version.new("6.1.0")
+ db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).first
+ ActiveRecord::Tasks::DatabaseTasks.create(db_config)
else
- if defined?(JRUBY_VERSION)
- db_config.symbolize_keys!
- db_config[:configure_connection] = false
+ db_config = ActiveRecord::Base.configurations[Rails.env].clone
+ db_type = db_config["adapter"]
+ db_name = db_config.delete("database")
+ raise StandardError.new("No database name specified.") if db_name.blank?
+ if db_type == "sqlite3"
+ db_file = Pathname.new(__FILE__).dirname.join(db_name)
+ db_file.unlink if db_file.file?
+ else
+ if defined?(JRUBY_VERSION)
+ db_config.symbolize_keys!
+ db_config[:configure_connection] = false
+ end
+ adapter = ActiveRecord::Base.send("#{db_type}_connection", db_config)
+ adapter.recreate_database db_name, db_config.slice("charset").symbolize_keys
+ adapter.disconnect!
end
- adapter = ActiveRecord::Base.send("#{db_type}_connection", db_config)
- adapter.recreate_database db_name, db_config.slice('charset').symbolize_keys
- adapter.disconnect!
end
rescue => e
Kernel.warn e
end
-logfile = Pathname.new(__FILE__).dirname.join('debug.log')
+logfile = Pathname.new(__FILE__).dirname.join("debug.log")
logfile.unlink if logfile.file?
ActiveRecord::Base.logger = Logger.new(logfile)
@@ -41,6 +46,8 @@
t.column :created_at, :datetime
t.column :updated_at, :datetime
t.column :favourite_device, :string
+ t.column :ssn, :integer
+ t.column :phone_numbers, :string
end
create_table :companies do |t|
@@ -76,9 +83,9 @@
t.column :service_name, :string
end
- add_index :audits, [:auditable_id, :auditable_type], name: 'auditable_index'
- add_index :audits, [:associated_id, :associated_type], name: 'associated_index'
- add_index :audits, [:user_id, :user_type], name: 'user_index'
+ add_index :audits, [:auditable_id, :auditable_type], name: "auditable_index"
+ add_index :audits, [:associated_id, :associated_type], name: "associated_index"
+ add_index :audits, [:user_id, :user_type], name: "user_index"
add_index :audits, :request_uuid
add_index :audits, :created_at
add_index :audits, :service_name
diff --git a/test/db/version_1.rb b/test/db/version_1.rb
index fe1d24cb5..d0c94d58a 100644
--- a/test/db/version_1.rb
+++ b/test/db/version_1.rb
@@ -11,7 +11,7 @@
t.column :created_at, :datetime
end
- add_index :audits, [:auditable_id, :auditable_type], name: 'auditable_index'
- add_index :audits, [:user_id, :user_type], name: 'user_index'
+ add_index :audits, [:auditable_id, :auditable_type], name: "auditable_index"
+ add_index :audits, [:user_id, :user_type], name: "user_index"
add_index :audits, :created_at
end
diff --git a/test/db/version_2.rb b/test/db/version_2.rb
index 6396fc350..57b5a211d 100644
--- a/test/db/version_2.rb
+++ b/test/db/version_2.rb
@@ -12,7 +12,7 @@
t.column :created_at, :datetime
end
- add_index :audits, [:auditable_id, :auditable_type], name: 'auditable_index'
- add_index :audits, [:user_id, :user_type], name: 'user_index'
+ add_index :audits, [:auditable_id, :auditable_type], name: "auditable_index"
+ add_index :audits, [:user_id, :user_type], name: "user_index"
add_index :audits, :created_at
end
diff --git a/test/db/version_3.rb b/test/db/version_3.rb
index e04cf4336..d338c6c18 100644
--- a/test/db/version_3.rb
+++ b/test/db/version_3.rb
@@ -12,8 +12,7 @@
t.column :created_at, :datetime
end
- add_index :audits, [:auditable_id, :auditable_type], name: 'auditable_index'
- add_index :audits, [:user_id, :user_type], name: 'user_index'
+ add_index :audits, [:auditable_id, :auditable_type], name: "auditable_index"
+ add_index :audits, [:user_id, :user_type], name: "user_index"
add_index :audits, :created_at
end
-
diff --git a/test/db/version_4.rb b/test/db/version_4.rb
index 64cfeb464..9fde5fc45 100644
--- a/test/db/version_4.rb
+++ b/test/db/version_4.rb
@@ -13,8 +13,7 @@
t.column :remote_address, :string
end
- add_index :audits, [:auditable_id, :auditable_type], name: 'auditable_index'
- add_index :audits, [:user_id, :user_type], name: 'user_index'
+ add_index :audits, [:auditable_id, :auditable_type], name: "auditable_index"
+ add_index :audits, [:user_id, :user_type], name: "user_index"
add_index :audits, :created_at
end
-
diff --git a/test/db/version_5.rb b/test/db/version_5.rb
index 1d3bb2677..add72bec6 100644
--- a/test/db/version_5.rb
+++ b/test/db/version_5.rb
@@ -15,4 +15,3 @@
t.column :association_type, :string
end
end
-
diff --git a/test/db/version_6.rb b/test/db/version_6.rb
index 281ea9227..f91570183 100644
--- a/test/db/version_6.rb
+++ b/test/db/version_6.rb
@@ -15,5 +15,5 @@
t.column :associated_type, :string
end
- add_index :audits, [:auditable_type, :auditable_id], name: 'auditable_index'
+ add_index :audits, [:auditable_type, :auditable_id], name: "auditable_index"
end
diff --git a/test/install_generator_test.rb b/test/install_generator_test.rb
index 7d3c1bd26..5c1c36d0a 100644
--- a/test/install_generator_test.rb
+++ b/test/install_generator_test.rb
@@ -1,9 +1,9 @@
-require 'test_helper'
+require "test_helper"
-require 'generators/audited/install_generator'
+require "generators/audited/install_generator"
class InstallGeneratorTest < Rails::Generators::TestCase
- destination File.expand_path('../../tmp', __FILE__)
+ destination File.expand_path("../../tmp", __FILE__)
setup :prepare_destination
tests Audited::Generators::InstallGenerator
@@ -11,44 +11,44 @@ class InstallGeneratorTest < Rails::Generators::TestCase
run_generator
assert_migration "db/migrate/install_audited.rb" do |content|
- assert_includes(content, 'class InstallAudited')
- assert_includes(content, 't.column :audited_changes, :text')
+ assert_includes(content, "class InstallAudited")
+ assert_includes(content, "t.column :audited_changes, :text")
end
end
test "generate migration with 'jsonb' type for audited_changes column" do
- run_generator %w(--audited-changes-column-type jsonb)
+ run_generator %w[--audited-changes-column-type jsonb]
assert_migration "db/migrate/install_audited.rb" do |content|
- assert_includes(content, 'class InstallAudited')
- assert_includes(content, 't.column :audited_changes, :jsonb')
+ assert_includes(content, "class InstallAudited")
+ assert_includes(content, "t.column :audited_changes, :jsonb")
end
end
test "generate migration with 'json' type for audited_changes column" do
- run_generator %w(--audited-changes-column-type json)
+ run_generator %w[--audited-changes-column-type json]
assert_migration "db/migrate/install_audited.rb" do |content|
- assert_includes(content, 'class InstallAudited')
- assert_includes(content, 't.column :audited_changes, :json')
+ assert_includes(content, "class InstallAudited")
+ assert_includes(content, "t.column :audited_changes, :json")
end
end
test "generate migration with 'string' type for user_id column" do
- run_generator %w(--audited-user-id-column-type string)
+ run_generator %w[--audited-user-id-column-type string]
assert_migration "db/migrate/install_audited.rb" do |content|
- assert_includes(content, 'class InstallAudited')
- assert_includes(content, 't.column :user_id, :string')
+ assert_includes(content, "class InstallAudited")
+ assert_includes(content, "t.column :user_id, :string")
end
end
test "generate migration with 'uuid' type for user_id column" do
- run_generator %w(--audited-user-id-column-type uuid)
+ run_generator %w[--audited-user-id-column-type uuid]
assert_migration "db/migrate/install_audited.rb" do |content|
- assert_includes(content, 'class InstallAudited')
- assert_includes(content, 't.column :user_id, :uuid')
+ assert_includes(content, "class InstallAudited")
+ assert_includes(content, "t.column :user_id, :uuid")
end
end
@@ -56,8 +56,7 @@ class InstallGeneratorTest < Rails::Generators::TestCase
run_generator
assert_migration "db/migrate/install_audited.rb" do |content|
- parent = Rails::VERSION::MAJOR == 4 ? 'ActiveRecord::Migration' : "ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]"
- assert_includes(content, "class InstallAudited < #{parent}\n")
+ assert_includes(content, "class InstallAudited < ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]\n")
end
end
end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 915512670..0104d199e 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -1,18 +1,18 @@
-ENV['RAILS_ENV'] = 'gemtest'
+ENV["RAILS_ENV"] = "test"
$LOAD_PATH.unshift File.dirname(__FILE__)
-require File.expand_path('../../spec/rails_app/config/environment', __FILE__)
-require 'rails/test_help'
+require File.expand_path("../../spec/rails_app/config/environment", __FILE__)
+require "rails/test_help"
-require 'audited'
+require "audited"
class ActiveSupport::TestCase
setup do
ActiveRecord::Migration.verbose = false
end
- def load_schema( version )
+ def load_schema(version)
load File.dirname(__FILE__) + "/db/version_#{version}.rb"
end
end
diff --git a/test/upgrade_generator_test.rb b/test/upgrade_generator_test.rb
index 376710804..3ec3a6b83 100644
--- a/test/upgrade_generator_test.rb
+++ b/test/upgrade_generator_test.rb
@@ -1,21 +1,17 @@
-require 'test_helper'
+require "test_helper"
-require 'generators/audited/upgrade_generator'
+require "generators/audited/upgrade_generator"
class UpgradeGeneratorTest < Rails::Generators::TestCase
- destination File.expand_path('../../tmp', __FILE__)
+ destination File.expand_path("../../tmp", __FILE__)
setup :prepare_destination
tests Audited::Generators::UpgradeGenerator
- if Rails::VERSION::MAJOR == 4
- self.use_transactional_fixtures = false
- else
- self.use_transactional_tests = false
- end
+ self.use_transactional_tests = false
test "should add 'comment' to audits table" do
load_schema 1
- run_generator %w(upgrade)
+ run_generator %w[upgrade]
assert_migration "db/migrate/add_comment_to_audits.rb" do |content|
assert_match(/add_column :audits, :comment, :string/, content)
@@ -27,7 +23,7 @@ class UpgradeGeneratorTest < Rails::Generators::TestCase
test "should rename 'changes' to 'audited_changes'" do
load_schema 2
- run_generator %w(upgrade)
+ run_generator %w[upgrade]
assert_no_migration "db/migrate/add_comment_to_audits.rb"
@@ -39,7 +35,7 @@ class UpgradeGeneratorTest < Rails::Generators::TestCase
test "should add a 'remote_address' to audits table" do
load_schema 3
- run_generator %w(upgrade)
+ run_generator %w[upgrade]
assert_migration "db/migrate/add_remote_address_to_audits.rb" do |content|
assert_match(/add_column :audits, :remote_address, :string/, content)
@@ -49,7 +45,7 @@ class UpgradeGeneratorTest < Rails::Generators::TestCase
test "should add 'association_id' and 'association_type' to audits table" do
load_schema 4
- run_generator %w(upgrade)
+ run_generator %w[upgrade]
assert_migration "db/migrate/add_association_to_audits.rb" do |content|
assert_match(/add_column :audits, :association_id, :integer/, content)
@@ -60,7 +56,7 @@ class UpgradeGeneratorTest < Rails::Generators::TestCase
test "should rename 'association_id' to 'associated_id' and 'association_type' to 'associated_type'" do
load_schema 5
- run_generator %w(upgrade)
+ run_generator %w[upgrade]
assert_migration "db/migrate/rename_association_to_associated.rb" do |content|
assert_match(/rename_column :audits, :association_id, :associated_id/, content)
@@ -71,7 +67,7 @@ class UpgradeGeneratorTest < Rails::Generators::TestCase
test "should add 'request_uuid' to audits table" do
load_schema 6
- run_generator %w(upgrade)
+ run_generator %w[upgrade]
assert_migration "db/migrate/add_request_uuid_to_audits.rb" do |content|
assert_match(/add_column :audits, :request_uuid, :string/, content)
@@ -82,7 +78,7 @@ class UpgradeGeneratorTest < Rails::Generators::TestCase
test "should add 'version' to auditable_index" do
load_schema 6
- run_generator %w(upgrade)
+ run_generator %w[upgrade]
assert_migration "db/migrate/add_version_to_auditable_index.rb" do |content|
assert_match(/add_index :audits, \[:auditable_type, :auditable_id, :version\]/, content)
@@ -92,11 +88,10 @@ class UpgradeGeneratorTest < Rails::Generators::TestCase
test "generate migration with correct AR migration parent" do
load_schema 1
- run_generator %w(upgrade)
+ run_generator %w[upgrade]
assert_migration "db/migrate/add_comment_to_audits.rb" do |content|
- parent = Rails::VERSION::MAJOR == 4 ? 'ActiveRecord::Migration' : "ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]"
- assert_includes(content, "class AddCommentToAudits < #{parent}\n")
+ assert_includes(content, "class AddCommentToAudits < ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]\n")
end
end
end