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