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 [![Build Status](https://secure.travis-ci.org/collectiveidea/audited.svg)](http://travis-ci.org/collectiveidea/audited) [![Code Climate](https://codeclimate.com/github/collectiveidea/audited.svg)](https://codeclimate.com/github/collectiveidea/audited) [![Security](https://hakiri.io/github/collectiveidea/audited/master.svg)](https://hakiri.io/github/collectiveidea/audited/master) -======= +Audited +[![Gem Version](https://img.shields.io/gem/v/audited.svg)](http://rubygems.org/gems/audited) +![Build Status](https://github.com/collectiveidea/audited/actions/workflows/ci.yml/badge.svg) +[![Code Climate](https://codeclimate.com/github/collectiveidea/audited.svg)](https://codeclimate.com/github/collectiveidea/audited) +[![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](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