diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index dff3b543..41223030 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -32,11 +32,11 @@ require "bundler/inline" # STEP ONE: What versions are you using? gemfile(true) do - ruby "2.5.1" + ruby "3.0.2" source "https://rubygems.org" - gem "activerecord", "5.2.0" + gem "activerecord", "6.1.4.1" gem "minitest", "5.11.3" - gem "paper_trail", "9.2.0", require: false + gem "paper_trail", "12.1.0", require: false gem "sqlite3", "1.3.13" end diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8ecdb41b..c260fdd0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: uses: ruby/setup-ruby@v1 with: # See "Lowest supported ruby version" in CONTRIBUTING.md - ruby-version: '2.5' + ruby-version: '2.6' - name: Bundle run: | gem install bundler @@ -63,13 +63,15 @@ jobs: # in case it still produces any deprecation warnings. # # See "Lowest supported ruby version" in CONTRIBUTING.md - ruby: [ '2.5', '2.7', '3.0' ] + ruby: [ '2.6', '2.7', '3.0', '3.1' ] exclude: # rails 5.2 requires ruby < 3.0 # https://github.com/rails/rails/issues/40938 - ruby: '3.0' gemfile: 'rails_5.2' + - ruby: '3.1' + gemfile: 'rails_5.2' steps: - name: Checkout source uses: actions/checkout@v2 diff --git a/.rubocop.yml b/.rubocop.yml index 549ef8b2..226b6498 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -14,25 +14,24 @@ inherit_from: .rubocop_todo.yml # - Only include permanent config; temporary goes in .rubocop_todo.yml AllCops: + # Generated files, like schema.rb, are out of our control. Exclude: - - gemfiles/vendor/bundle/**/* # This dir only shows up on travis ¯\_(ツ)_/¯ - - spec/dummy_app/db/schema.rb # Generated, out of our control + - gemfiles/* + - spec/dummy_app/db/schema.rb # Enable pending cops so we can adopt the code before they are switched on. NewCops: enable # See "Lowest supported ruby version" in CONTRIBUTING.md - TargetRubyVersion: 2.5 - -Bundler/OrderedGems: - Exclude: - - gemfiles/* # generated by Appraisal + TargetRubyVersion: 2.6 Layout/ArgumentAlignment: EnforcedStyle: with_fixed_indentation +# This cop has a bug in 1.22.2 (https://github.com/rubocop/rubocop/issues/10210) +# When the bug is fixed, we'll return to using `EnforcedStyle: trailing`. Layout/DotPosition: - EnforcedStyle: trailing + Enabled: false # Avoid blank lines inside methods. They are a sign that the method is too big. Layout/EmptyLineAfterGuardClause: @@ -57,20 +56,11 @@ Layout/MultilineOperationIndentation: Layout/ParameterAlignment: EnforcedStyle: with_fixed_indentation -Layout/SpaceAroundMethodCallOperator: - Enabled: true - # Use exactly one space on each side of an operator. Do not align operators # because it makes the code harder to edit, and makes lines unnecessarily long. Layout/SpaceAroundOperators: AllowForAlignment: false -Lint/RaiseException: - Enabled: true - -Lint/StructNewOverride: - Enabled: true - # Migrations often contain long up/down methods, and extracting smaller methods # from these is of questionable value. Metrics/AbcSize: @@ -100,12 +90,9 @@ Naming/FileName: - Appraisals # Heredocs are usually assigned to a variable or constant, which already has a -# name, so naming the heredoc doesn't add much value. Feel free to name -# heredocs that are used as anonymous values (not a variable, constant, or -# named parameter). -# -# All heredocs containing SQL should be named SQL, to support editor syntax -# highlighting. +# name, so naming the delimiter doesn't add much value unless doing so improves +# syntax highlighting. For example, all heredocs containing SQL should be named +# SQL, to support editor syntax highlighting. Naming/HeredocDelimiterNaming: Enabled: false @@ -136,11 +123,6 @@ Rails/SkipsModelValidations: RSpec/DescribeClass: Enabled: false -# This cop has a bug in 1.35.0 -# https://github.com/rubocop-hq/rubocop-rspec/issues/799 -RSpec/DescribedClass: - Enabled: false - # Yes, ideally examples would be short. Is it possible to pick a limit and say, # "no example will ever be longer than this"? Hard to say. Sometimes they're # quite long. @@ -164,10 +146,6 @@ Style/BlockDelimiters: Style/DoubleNegation: Enabled: false -# This cop is unimportant in this repo. -Style/ExponentialNotation: - Enabled: false - # Avoid annotated tokens except in desperately complicated format strings. # In 99% of format strings they actually make it less readable. Style/FormatStringToken: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 22d8b95e..a83dd96a 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -6,21 +6,6 @@ # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 5 -# Configuration parameters: IgnoredMethods, CountRepeatedAttributes. -Metrics/AbcSize: - Max: 17.5 # Goal: 17, the default - -# Offense count: 1 -# Configuration parameters: IgnoredMethods. -Metrics/CyclomaticComplexity: - Max: 8 # Goal: 7, the default - -# Offense count: 1 -# Configuration parameters: IgnoredMethods. -Metrics/PerceivedComplexity: - Max: 9 # Goal: 8, the default - # Offense count: 56 # Cop supports --auto-correct. Rails/ApplicationRecord: diff --git a/Appraisals b/Appraisals index b4051e70..7d16c71c 100644 --- a/Appraisals +++ b/Appraisals @@ -24,3 +24,8 @@ appraise "rails-6.1" do gem "rails", "~> 6.1.0" gem "rails-controller-testing", "~> 1.0.5" end + +appraise "rails-7.0" do + gem "rails", "~> 7.0.0" + gem "rails-controller-testing", "~> 1.0.5" +end diff --git a/CHANGELOG.md b/CHANGELOG.md index 504c2185..be3f4129 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,29 @@ recommendations of [keepachangelog.com](http://keepachangelog.com/). - None +## 12.2.0 (2022-01-21) + +### Breaking Changes + +- None + +### Added + +- [#1365](https://github.com/paper-trail-gem/paper_trail/pull/1365) - + Support Rails 7.0 +- [#1349](https://github.com/paper-trail-gem/paper_trail/pull/1349) - + `if:` and `unless:` work with `touch` events now. + +### Fixed + +- None + +### Dependencies + +- [#1338](https://github.com/paper-trail-gem/paper_trail/pull/1338) - + Support Psych version 4 +- ruby >= 2.6 (was >= 2.5). Ruby 2.5 reached EoL on 2021-03-31. + ## 12.1.0 (2021-08-30) ### Breaking Changes diff --git a/README.md b/README.md index 6b62cc51..627fec76 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ This is the _user guide_. See also, the Choose version: [Unreleased](https://github.com/paper-trail-gem/paper_trail/blob/master/README.md), -[12.1](https://github.com/paper-trail-gem/paper_trail/blob/v12.1.0/README.md), +[12.2](https://github.com/paper-trail-gem/paper_trail/blob/v12.2.0/README.md), [11.1](https://github.com/paper-trail-gem/paper_trail/blob/v11.1.0/README.md), [10.3](https://github.com/paper-trail-gem/paper_trail/blob/v10.3.1/README.md), [9.2](https://github.com/paper-trail-gem/paper_trail/blob/v9.2.0/README.md), @@ -1208,17 +1208,20 @@ class PostVersion < PaperTrail::Version end ``` -If you only use custom version classes and don't have a `versions` table, you -must let ActiveRecord know that the `PaperTrail::Version` class is an -`abstract_class`. +If you only use custom version classes and don't have a `versions` table, you must +let ActiveRecord know that your base version class (eg. `ApplicationVersion` below) +class is an `abstract_class`. ```ruby -# app/models/paper_trail/version.rb -module PaperTrail - class Version < ActiveRecord::Base - include PaperTrail::VersionConcern - self.abstract_class = true - end +# app/models/application_version.rb +class ApplicationVersion < ActiveRecord::Base + include PaperTrail::VersionConcern + self.abstract_class = true +end + +class PostVersion < ApplicationVersion + self.table_name = :post_versions + self.sequence_name = :post_versions_id_seq end ``` diff --git a/Rakefile b/Rakefile index 3f814c5f..0c7ed0c0 100644 --- a/Rakefile +++ b/Rakefile @@ -38,11 +38,8 @@ task :clean do end end -desc <<~EOS - Write a database.yml for the specified RDBMS, and create database. Does not - migrate. Migration happens later in spec_helper. -EOS -task prepare: %i[clean install_database_yml] do +desc "Create the database." +task :create_db do puts format("creating %s database", ENV["DB"]) case ENV["DB"] when "mysql" @@ -59,6 +56,12 @@ task prepare: %i[clean install_database_yml] do end end +desc <<~EOS + Write a database.yml for the specified RDBMS, and create database. Does not + migrate. Migration happens later in spec_helper. +EOS +task prepare: %i[clean install_database_yml create_db] + require "rspec/core/rake_task" desc "Run tests on PaperTrail with RSpec" task(:spec).clear diff --git a/gemfiles/rails_7.0.gemfile b/gemfiles/rails_7.0.gemfile new file mode 100644 index 00000000..ed927c64 --- /dev/null +++ b/gemfiles/rails_7.0.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rails", "~> 7.0.0" +gem "rails-controller-testing", "~> 1.0.5" + +gemspec path: "../" diff --git a/lib/generators/paper_trail/install/templates/create_versions.rb.erb b/lib/generators/paper_trail/install/templates/create_versions.rb.erb index 268d9290..677d1b73 100644 --- a/lib/generators/paper_trail/install/templates/create_versions.rb.erb +++ b/lib/generators/paper_trail/install/templates/create_versions.rb.erb @@ -28,7 +28,9 @@ class CreateVersions < ActiveRecord::Migration<%= migration_version %> # MySQL users should also upgrade to at least rails 4.2, which is the first # version of ActiveRecord with support for fractional seconds in MySQL. # (https://github.com/rails/rails/pull/14359) - # + # + # MySQL users should use the following line for `created_at` + # t.datetime :created_at, limit: 6 t.datetime :created_at end add_index :versions, %i(item_type item_id) diff --git a/lib/paper_trail.rb b/lib/paper_trail.rb index 5df47023..8559f556 100644 --- a/lib/paper_trail.rb +++ b/lib/paper_trail.rb @@ -26,6 +26,8 @@ module PaperTrail named created_at. EOS + RAILS_GTE_7_0 = ::ActiveRecord.gem_version >= ::Gem::Version.new("7.0.0") + extend PaperTrail::Cleaner class << self diff --git a/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb b/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb index 5fcdb05c..471a2ce8 100644 --- a/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +++ b/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb @@ -32,6 +32,12 @@ module PaperTrail if defined_enums[attr] && val.is_a?(::String) # Because PT 4 used to save the string version of enums to `object_changes` val + elsif PaperTrail::RAILS_GTE_7_0 && val.is_a?(ActiveRecord::Type::Time::Value) + # Because Rails 7 time attribute throws a delegation error when you deserialize + # it with the factory. + # See ActiveRecord::Type::Time::Value crashes when loaded from YAML on rails 7.0 + # https://github.com/rails/rails/issues/43966 + val.instance_variable_get(:@time) else AttributeSerializerFactory.for(@klass, attr).deserialize(val) end diff --git a/lib/paper_trail/compatibility.rb b/lib/paper_trail/compatibility.rb index 85f74cb7..5a5ee0df 100644 --- a/lib/paper_trail/compatibility.rb +++ b/lib/paper_trail/compatibility.rb @@ -8,7 +8,7 @@ module PaperTrail # # It is not safe to assume that a new version of rails will be compatible with # PaperTrail. PT is only compatible with the versions of rails that it is - # tested against. See `.travis.yml`. + # tested against. See `.github/workflows/test.yml`. # # However, as of # [#1213](https://github.com/paper-trail-gem/paper_trail/pull/1213) our @@ -18,7 +18,7 @@ module PaperTrail # versions. module Compatibility ACTIVERECORD_GTE = ">= 5.2" # enforced in gemspec - ACTIVERECORD_LT = "< 7.0" # not enforced in gemspec + ACTIVERECORD_LT = "< 7.1" # not enforced in gemspec E_INCOMPATIBLE_AR = <<-EOS PaperTrail %s is not compatible with ActiveRecord %s. We allow PT diff --git a/lib/paper_trail/events/base.rb b/lib/paper_trail/events/base.rb index 58fb259b..f50aa9ec 100644 --- a/lib/paper_trail/events/base.rb +++ b/lib/paper_trail/events/base.rb @@ -116,6 +116,20 @@ module PaperTrail @changes_in_latest_version ||= load_changes_in_latest_version end + # @api private + def evaluate_only + only = @record.paper_trail_options[:only].dup + # Remove Hash arguments and then evaluate whether the attributes (the + # keys of the hash) should also get pushed into the collection. + only.delete_if do |obj| + obj.is_a?(Hash) && + obj.each { |attr, condition| + only << attr if condition.respond_to?(:call) && condition.call(@record) + } + end + only + end + # An attributed is "ignored" if it is listed in the `:ignore` option # and/or the `:skip` option. Returns true if an ignored attribute has # changed. @@ -182,20 +196,28 @@ module PaperTrail if value.respond_to?(:call) value.call(@record) elsif value.is_a?(Symbol) && @record.respond_to?(value, true) - # If it is an attribute that is changing in an existing object, - # be sure to grab the current version. - if event != "create" && - @record.has_attribute?(value) && - attribute_changed_in_latest_version?(value) - attribute_in_previous_version(value, false) - else - @record.send(value) - end + metadatum_from_model_method(event, value) else value end end + # The model method can either be an attribute or a non-attribute method. + # + # If it is an attribute that is changing in an existing object, + # be sure to grab the correct version. + # + # @api private + def metadatum_from_model_method(event, method) + if event != "create" && + @record.has_attribute?(method) && + attribute_changed_in_latest_version?(method) + attribute_in_previous_version(method, false) + else + @record.send(method) + end + end + # @api private def notable_changes changes_in_latest_version.delete_if { |k, _v| @@ -207,16 +229,9 @@ module PaperTrail def notably_changed # Memoized to reduce memory usage @notably_changed ||= begin - only = @record.paper_trail_options[:only].dup - # Remove Hash arguments and then evaluate whether the attributes (the - # keys of the hash) should also get pushed into the collection. - only.delete_if do |obj| - obj.is_a?(Hash) && - obj.each { |attr, condition| - only << attr if condition.respond_to?(:call) && condition.call(@record) - } - end - only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only) + only = evaluate_only + cani = changed_and_not_ignored + only.empty? ? cani : (cani & only) end end diff --git a/lib/paper_trail/events/update.rb b/lib/paper_trail/events/update.rb index 8516bc34..89407226 100644 --- a/lib/paper_trail/events/update.rb +++ b/lib/paper_trail/events/update.rb @@ -35,16 +35,21 @@ module PaperTrail if record_object? data[:object] = recordable_object(@is_touch) end - if record_object_changes? - changes = @force_changes.nil? ? notable_changes : @force_changes - data[:object_changes] = prepare_object_changes(changes) - end + merge_object_changes_into(data) merge_item_subtype_into(data) merge_metadata_into(data) end private + # @api private + def merge_object_changes_into(data) + if record_object_changes? + changes = @force_changes.nil? ? notable_changes : @force_changes + data[:object_changes] = prepare_object_changes(changes) + end + end + # `touch` cannot record `object_changes` because rails' `touch` does not # perform dirty-tracking. Specifically, methods from `Dirty`, like # `saved_changes`, return the same values before and after `touch`. diff --git a/lib/paper_trail/model_config.rb b/lib/paper_trail/model_config.rb index 60cef85a..41bbcbca 100644 --- a/lib/paper_trail/model_config.rb +++ b/lib/paper_trail/model_config.rb @@ -40,8 +40,7 @@ module PaperTrail @model_class.after_create { |r| r.paper_trail.record_create if r.paper_trail.save_version? } - return if @model_class.paper_trail_options[:on].include?(:create) - @model_class.paper_trail_options[:on] << :create + append_option_uniquely(:on, :create) end # Adds a callback that records a version before or after a "destroy" event. @@ -49,7 +48,6 @@ module PaperTrail # @api public def on_destroy(recording_order = "before") assert_valid_recording_order_for_on_destroy(recording_order) - @model_class.send( "#{recording_order}_destroy", lambda do |r| @@ -57,9 +55,7 @@ module PaperTrail r.paper_trail.record_destroy(recording_order) end ) - - return if @model_class.paper_trail_options[:on].include?(:destroy) - @model_class.paper_trail_options[:on] << :destroy + append_option_uniquely(:on, :destroy) end # Adds a callback that records a version after an "update" event. @@ -81,8 +77,7 @@ module PaperTrail @model_class.after_update { |r| r.paper_trail.clear_version_instance } - return if @model_class.paper_trail_options[:on].include?(:update) - @model_class.paper_trail_options[:on] << :update + append_option_uniquely(:on, :update) end # Adds a callback that records a version after a "touch" event. @@ -96,11 +91,13 @@ module PaperTrail # @api public def on_touch @model_class.after_touch { |r| - r.paper_trail.record_update( - force: RAILS_LT_6_0, - in_after_callback: true, - is_touch: true - ) + if r.paper_trail.save_version? + r.paper_trail.record_update( + force: RAILS_LT_6_0, + in_after_callback: true, + is_touch: true + ) + end } end @@ -127,6 +124,13 @@ module PaperTrail RAILS_LT_6_0 = ::ActiveRecord.gem_version < ::Gem::Version.new("6.0.0") private_constant :RAILS_LT_6_0 + # @api private + def append_option_uniquely(option, value) + collection = @model_class.paper_trail_options.fetch(option) + return if collection.include?(value) + collection << value + end + # Raises an error if the provided class is an `abstract_class`. # @api private def assert_concrete_activerecord_class(class_name) @@ -205,6 +209,14 @@ module PaperTrail options end + # Process an `ignore`, `skip`, or `only` option. + def event_attribute_option(option_name) + [@model_class.paper_trail_options[option_name]]. + flatten. + compact. + map { |attr| attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s } + end + def get_versions_scope(options) options[:versions][:scope] || -> { order(model.timestamp_sort_order) } end @@ -239,12 +251,8 @@ module PaperTrail @model_class.paper_trail_options = options.dup %i[ignore skip only].each do |k| - @model_class.paper_trail_options[k] = [@model_class.paper_trail_options[k]]. - flatten. - compact. - map { |attr| attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s } + @model_class.paper_trail_options[k] = event_attribute_option(k) end - @model_class.paper_trail_options[:meta] ||= {} end end diff --git a/lib/paper_trail/serializers/yaml.rb b/lib/paper_trail/serializers/yaml.rb index 31ffc9d8..0530c990 100644 --- a/lib/paper_trail/serializers/yaml.rb +++ b/lib/paper_trail/serializers/yaml.rb @@ -9,7 +9,7 @@ module PaperTrail extend self # makes all instance methods become module methods as well def load(string) - ::YAML.load string + ::YAML.respond_to?(:unsafe_load) ? ::YAML.unsafe_load(string) : ::YAML.load(string) end # @param object (Hash | HashWithIndifferentAccess) - Coming from diff --git a/lib/paper_trail/version_concern.rb b/lib/paper_trail/version_concern.rb index 1c5e5244..1e4a96c0 100644 --- a/lib/paper_trail/version_concern.rb +++ b/lib/paper_trail/version_concern.rb @@ -16,7 +16,7 @@ module PaperTrail extend ::ActiveSupport::Concern included do - belongs_to :item, polymorphic: true, optional: true + belongs_to :item, polymorphic: true, optional: true, inverse_of: false validates_presence_of :event after_create :enforce_version_limit! end @@ -376,10 +376,11 @@ module PaperTrail # # @api private def version_limit - if limit_option?(item.class) - item.class.paper_trail_options[:limit] - elsif base_class_limit_option?(item.class) - item.class.base_class.paper_trail_options[:limit] + klass = item.class + if limit_option?(klass) + klass.paper_trail_options[:limit] + elsif base_class_limit_option?(klass) + klass.base_class.paper_trail_options[:limit] else PaperTrail.config.version_limit end diff --git a/lib/paper_trail/version_number.rb b/lib/paper_trail/version_number.rb index 97a5be13..0b24f405 100644 --- a/lib/paper_trail/version_number.rb +++ b/lib/paper_trail/version_number.rb @@ -8,7 +8,7 @@ module PaperTrail # People are encouraged to use `PaperTrail.gem_version` instead. module VERSION MAJOR = 12 - MINOR = 1 + MINOR = 2 TINY = 0 # Set PRE to nil unless it's a pre-release (beta, rc, etc.) diff --git a/paper_trail.gemspec b/paper_trail.gemspec index e04f8f86..b2f61093 100644 --- a/paper_trail.gemspec +++ b/paper_trail.gemspec @@ -43,9 +43,7 @@ has been destroyed. # about 3 years, per https://www.ruby-lang.org/en/downloads/branches/ # # See "Lowest supported ruby version" in CONTRIBUTING.md - # - # Ruby 2.5 reaches EoL on 2021-03-31. - s.required_ruby_version = ">= 2.5.0" + s.required_ruby_version = ">= 2.6.0" # We no longer specify a maximum activerecord version. # See discussion in paper_trail/compatibility.rb @@ -53,8 +51,8 @@ has been destroyed. s.add_dependency "request_store", "~> 1.1" s.add_development_dependency "appraisal", "~> 2.4.1" - s.add_development_dependency "byebug", "~> 11.0" - s.add_development_dependency "ffaker", "~> 2.19.0" + s.add_development_dependency "byebug", "~> 11.1" + s.add_development_dependency "ffaker", "~> 2.20" s.add_development_dependency "generator_spec", "~> 0.9.4" s.add_development_dependency "memory_profiler", "~> 1.0.0" @@ -65,13 +63,13 @@ has been destroyed. s.add_development_dependency "rake", "~> 13.0" s.add_development_dependency "rspec-rails", "~> 5.0.2" - s.add_development_dependency "rubocop", "~> 1.20.0" + s.add_development_dependency "rubocop", "~> 1.22.2" s.add_development_dependency "rubocop-packaging", "~> 0.5.1" s.add_development_dependency "rubocop-performance", "~> 1.11.5" - s.add_development_dependency "rubocop-rails", "~> 2.11.3" + s.add_development_dependency "rubocop-rails", "~> 2.12.4" s.add_development_dependency "rubocop-rake", "~> 0.6.0" - s.add_development_dependency "rubocop-rspec", "~> 2.4.0" - s.add_development_dependency "simplecov", ">= 0.21", "< 0.22" + s.add_development_dependency "rubocop-rspec", "~> 2.5.0" + s.add_development_dependency "simplecov", "~> 0.21.2" # ## Database Adapters # @@ -83,7 +81,7 @@ has been destroyed. # Currently, all versions of rails we test against are consistent. In the past, # when we tested against rails 4.2, we had to specify database adapters in # `Appraisals`. - s.add_development_dependency "mysql2", "~> 0.5" - s.add_development_dependency "pg", ">= 0.18", "< 2.0" + s.add_development_dependency "mysql2", "~> 0.5.3" + s.add_development_dependency "pg", "~> 1.2" s.add_development_dependency "sqlite3", "~> 1.4" end diff --git a/spec/dummy_app/app/models/car.rb b/spec/dummy_app/app/models/car.rb index ea3faa5d..a6064a83 100644 --- a/spec/dummy_app/app/models/car.rb +++ b/spec/dummy_app/app/models/car.rb @@ -2,4 +2,6 @@ class Car < Vehicle has_paper_trail + attribute :color, type: ActiveModel::Type::String + attr_accessor :top_speed end diff --git a/spec/dummy_app/app/models/fruit.rb b/spec/dummy_app/app/models/fruit.rb index 1237612c..ce119127 100644 --- a/spec/dummy_app/app/models/fruit.rb +++ b/spec/dummy_app/app/models/fruit.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true +# See also `Vegetable` which uses `JsonbVersion`. class Fruit < ActiveRecord::Base - if ENV["DB"] == "postgres" || JsonVersion.table_exists? + if ENV["DB"] == "postgres" has_paper_trail versions: { class_name: "JsonVersion" } end end diff --git a/spec/dummy_app/app/models/translation.rb b/spec/dummy_app/app/models/translation.rb index cdcc3b3b..b1c00e5e 100644 --- a/spec/dummy_app/app/models/translation.rb +++ b/spec/dummy_app/app/models/translation.rb @@ -2,12 +2,8 @@ # Demonstrates the `if` and `unless` configuration options. class Translation < ActiveRecord::Base - # Has a `type` column, but it's not used for STI. - # TODO: rename column - self.inheritance_column = nil - has_paper_trail( if: proc { |t| t.language_code == "US" }, - unless: proc { |t| t.type == "DRAFT" } + unless: proc { |t| t.draft_status == "DRAFT" } ) end diff --git a/spec/dummy_app/app/models/vegetable.rb b/spec/dummy_app/app/models/vegetable.rb new file mode 100644 index 00000000..a07a24c6 --- /dev/null +++ b/spec/dummy_app/app/models/vegetable.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# See also `Fruit` which uses `JsonVersion`. +class Vegetable < ActiveRecord::Base + has_paper_trail versions: { + class_name: ENV["DB"] == "postgres" ? "JsonbVersion" : "PaperTrail::Version" + }, on: %i[create update] +end diff --git a/spec/dummy_app/app/versions/jsonb_version.rb b/spec/dummy_app/app/versions/jsonb_version.rb new file mode 100644 index 00000000..59040471 --- /dev/null +++ b/spec/dummy_app/app/versions/jsonb_version.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class JsonbVersion < ActiveRecord::Base + include PaperTrail::VersionConcern + + self.table_name = "jsonb_versions" +end diff --git a/spec/dummy_app/db/migrate/20110208155312_set_up_test_tables.rb b/spec/dummy_app/db/migrate/20110208155312_set_up_test_tables.rb index 9505b581..377889ad 100644 --- a/spec/dummy_app/db/migrate/20110208155312_set_up_test_tables.rb +++ b/spec/dummy_app/db/migrate/20110208155312_set_up_test_tables.rb @@ -128,16 +128,19 @@ class SetUpTestTables < ::ActiveRecord::Migration::Current add_index :no_object_versions, %i[item_type item_id] if ENV["DB"] == "postgres" - create_table :json_versions, force: true do |t| - t.string :item_type, null: false - t.integer :item_id, null: false - t.string :event, null: false - t.string :whodunnit - t.json :object - t.json :object_changes - t.datetime :created_at, limit: 6 + %w[json jsonb].each do |j| + table_name = j + "_versions" + create_table table_name, force: true do |t| + t.string :item_type, null: false + t.bigint :item_id, null: false + t.string :event, null: false + t.string :whodunnit + t.public_send j, :object + t.public_send j, :object_changes + t.datetime :created_at, limit: 6 + end + add_index table_name, %i[item_type item_id] end - add_index :json_versions, %i[item_type item_id] end create_table :not_on_updates, force: true do |t| @@ -249,10 +252,10 @@ class SetUpTestTables < ::ActiveRecord::Migration::Current end create_table :translations, force: true do |t| - t.string :headline t.string :content + t.string :draft_status + t.string :headline t.string :language_code - t.string :type end create_table :gadgets, force: true do |t| @@ -277,8 +280,9 @@ class SetUpTestTables < ::ActiveRecord::Migration::Current end create_table :fruits, force: true do |t| - t.string :name t.string :color + t.integer :mass + t.string :name end create_table :boolits, force: true do |t| @@ -358,6 +362,12 @@ class SetUpTestTables < ::ActiveRecord::Migration::Current t.integer :parent_id t.integer :partner_id end + + create_table :vegetables, force: true do |t| + t.string :color + t.integer :mass + t.string :name + end end def down diff --git a/spec/models/animal_spec.rb b/spec/models/animal_spec.rb index e1e1ed62..20b74851 100644 --- a/spec/models/animal_spec.rb +++ b/spec/models/animal_spec.rb @@ -4,8 +4,8 @@ require "spec_helper" RSpec.describe Animal, type: :model, versioning: true do it "baseline test setup" do - expect(Animal.new).to be_versioned - expect(Animal.inheritance_column).to eq("species") + expect(described_class.new).to be_versioned + expect(described_class.inheritance_column).to eq("species") end describe "#descends_from_active_record?" do @@ -15,7 +15,7 @@ RSpec.describe Animal, type: :model, versioning: true do end it "works with custom STI inheritance column" do - animal = Animal.create(name: "Animal") + animal = described_class.create(name: "Animal") animal.update(name: "Animal from the Muppets") animal.update(name: "Animal Muppet") animal.destroy @@ -46,7 +46,7 @@ RSpec.describe Animal, type: :model, versioning: true do it "allows the inheritance_column (species) to be updated" do cat = Cat.create!(name: "Leo") cat.update(name: "Spike", species: "Dog") - dog = Animal.find(cat.id) + dog = described_class.find(cat.id) expect(dog).to be_instance_of(Dog) end diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb index 2fb0a998..d8ec2b14 100644 --- a/spec/models/article_spec.rb +++ b/spec/models/article_spec.rb @@ -187,7 +187,7 @@ RSpec.describe Article, type: :model, versioning: true do end context "with an item" do - let(:article) { Article.new(title: initial_title) } + let(:article) { described_class.new(title: initial_title) } let(:initial_title) { "Foobar" } context "when it is created" do diff --git a/spec/models/book_spec.rb b/spec/models/book_spec.rb index 77e57c7c..d51f15fd 100644 --- a/spec/models/book_spec.rb +++ b/spec/models/book_spec.rb @@ -5,7 +5,7 @@ require "spec_helper" RSpec.describe Book, versioning: true do context "with :has_many :through" do it "store version on source <<" do - book = Book.create(title: "War and Peace") + book = described_class.create(title: "War and Peace") dostoyevsky = Person.create(name: "Dostoyevsky") Person.create(name: "Solzhenitsyn") count = PaperTrail::Version.count @@ -15,7 +15,7 @@ RSpec.describe Book, versioning: true do end it "store version on source create" do - book = Book.create(title: "War and Peace") + book = described_class.create(title: "War and Peace") Person.create(name: "Dostoyevsky") Person.create(name: "Solzhenitsyn") count = PaperTrail::Version.count @@ -27,7 +27,7 @@ RSpec.describe Book, versioning: true do end it "store version on join destroy" do - book = Book.create(title: "War and Peace") + book = described_class.create(title: "War and Peace") dostoyevsky = Person.create(name: "Dostoyevsky") Person.create(name: "Solzhenitsyn") (book.authors << dostoyevsky) @@ -39,7 +39,7 @@ RSpec.describe Book, versioning: true do end it "store version on join clear" do - book = Book.create(title: "War and Peace") + book = described_class.create(title: "War and Peace") dostoyevsky = Person.create(name: "Dostoyevsky") Person.create(name: "Solzhenitsyn") book.authors << dostoyevsky @@ -53,7 +53,7 @@ RSpec.describe Book, versioning: true do context "when a persisted record is updated then destroyed" do it "has changes" do - book = Book.create! title: "A" + book = described_class.create! title: "A" changes = YAML.load book.versions.last.attributes["object_changes"] expect(changes).to eq("id" => [nil, book.id], "title" => [nil, "A"]) diff --git a/spec/models/boolit_spec.rb b/spec/models/boolit_spec.rb index 6e648906..c022bce7 100644 --- a/spec/models/boolit_spec.rb +++ b/spec/models/boolit_spec.rb @@ -4,7 +4,7 @@ require "spec_helper" require "support/custom_json_serializer" RSpec.describe Boolit, type: :model, versioning: true do - let(:boolit) { Boolit.create! } + let(:boolit) { described_class.create! } before { boolit.update!(name: FFaker::Name.name) } @@ -20,7 +20,7 @@ RSpec.describe Boolit, type: :model, versioning: true do before { boolit.update!(scoped: false) } it "is NOT scoped" do - expect(Boolit.first).to be_nil + expect(described_class.first).to be_nil end it "still can be reified and persisted" do diff --git a/spec/models/car_spec.rb b/spec/models/car_spec.rb index cf0e1d9d..37f98c88 100644 --- a/spec/models/car_spec.rb +++ b/spec/models/car_spec.rb @@ -7,9 +7,28 @@ RSpec.describe Car, type: :model do describe "changeset", versioning: true do it "has the expected keys (see issue 738)" do - car = Car.create!(name: "Alice") + car = described_class.create!(name: "Alice") car.update(name: "Bob") assert_includes car.versions.last.changeset.keys, "name" end end + + describe "attributes and accessors", versioning: true do + it "reifies attributes that are not AR attributes" do + car = described_class.create name: "Pinto", color: "green" + car.update color: "yellow" + car.update color: "brown" + expect(car.versions.second.reify.color).to eq("yellow") + end + + it "reifies attributes that once were attributes but now just attr_accessor" do + car = described_class.create name: "Pinto", color: "green" + car.update color: "yellow" + changes = PaperTrail::Serializers::YAML.load(car.versions.last.attributes["object"]) + changes[:top_speed] = 80 + car.versions.first.update object: changes.to_yaml + car.reload + expect(car.versions.first.reify.top_speed).to eq(80) + end + end end diff --git a/spec/models/custom_primary_key_record_spec.rb b/spec/models/custom_primary_key_record_spec.rb index 519f0ffe..1c1a125d 100644 --- a/spec/models/custom_primary_key_record_spec.rb +++ b/spec/models/custom_primary_key_record_spec.rb @@ -12,9 +12,9 @@ RSpec.describe CustomPrimaryKeyRecord, type: :model do version = custom_primary_key_record.versions.last expect(version).to be_a(CustomPrimaryKeyRecordVersion) version_from_db = CustomPrimaryKeyRecordVersion.last - expect(version_from_db.reify).to be_a(CustomPrimaryKeyRecord) + expect(version_from_db.reify).to be_a(described_class) custom_primary_key_record.destroy - expect(CustomPrimaryKeyRecordVersion.last.reify).to be_a(CustomPrimaryKeyRecord) + expect(CustomPrimaryKeyRecordVersion.last.reify).to be_a(described_class) end end end diff --git a/spec/models/document_spec.rb b/spec/models/document_spec.rb index 59e0a530..19bbe802 100644 --- a/spec/models/document_spec.rb +++ b/spec/models/document_spec.rb @@ -5,7 +5,7 @@ require "spec_helper" RSpec.describe Document, type: :model, versioning: true do describe "have_a_version_with matcher" do it "works with custom versions association" do - document = Document.create!(name: "Foo") + document = described_class.create!(name: "Foo") document.update!(name: "Bar") expect(document).to have_a_version_with(name: "Foo") end @@ -13,7 +13,7 @@ RSpec.describe Document, type: :model, versioning: true do describe "#paper_trail.next_version" do it "returns the expected document" do - doc = Document.create + doc = described_class.create doc.update(name: "Doc 1") reified = doc.paper_trail_versions.last.reify expect(doc.name).to(eq(reified.paper_trail.next_version.name)) @@ -22,7 +22,7 @@ RSpec.describe Document, type: :model, versioning: true do describe "#paper_trail.previous_version" do it "returns the expected document" do - doc = Document.create + doc = described_class.create doc.update(name: "Doc 1") doc.update(name: "Doc 2") expect(doc.paper_trail_versions.length).to(eq(3)) @@ -32,7 +32,7 @@ RSpec.describe Document, type: :model, versioning: true do describe "#paper_trail_versions" do it "returns the expected version records" do - doc = Document.create + doc = described_class.create doc.update(name: "Doc 1") expect(doc.paper_trail_versions.length).to(eq(2)) expect(doc.paper_trail_versions.map(&:event)).to( @@ -43,7 +43,7 @@ RSpec.describe Document, type: :model, versioning: true do describe "#versions" do it "does not respond to versions method" do - doc = Document.create + doc = described_class.create doc.update(name: "Doc 1") expect(doc).not_to respond_to(:versions) end diff --git a/spec/models/foo_widget_spec.rb b/spec/models/foo_widget_spec.rb index 4e571bb0..8e3bbfcf 100644 --- a/spec/models/foo_widget_spec.rb +++ b/spec/models/foo_widget_spec.rb @@ -5,7 +5,7 @@ require "support/performance_helpers" RSpec.describe(FooWidget, versioning: true) do context "with a subclass" do - let(:foo) { FooWidget.create } + let(:foo) { described_class.create } before do foo.update!(name: "Foo") @@ -26,7 +26,7 @@ RSpec.describe(FooWidget, versioning: true) do before { foo.destroy } it "reify with the correct type" do - assert_kind_of(FooWidget, foo.versions.last.reify) + assert_kind_of(described_class, foo.versions.last.reify) expect(PaperTrail::Version.last.previous).to(eq(foo.versions[1])) expect(PaperTrail::Version.last.next).to(be_nil) end diff --git a/spec/models/fruit_spec.rb b/spec/models/fruit_spec.rb index acb70fca..10e12bc9 100644 --- a/spec/models/fruit_spec.rb +++ b/spec/models/fruit_spec.rb @@ -2,19 +2,54 @@ require "spec_helper" -if ENV["DB"] == "postgres" || JsonVersion.table_exists? +if ENV["DB"] == "postgres" && JsonVersion.table_exists? RSpec.describe Fruit, type: :model, versioning: true do describe "have_a_version_with_changes matcher" do it "works with Fruit because Fruit uses JsonVersion" do # As of PT 9.0.0, with_version_changes only supports json(b) columns, # so that's why were testing the have_a_version_with_changes matcher # here. - banana = Fruit.create!(color: "Red", name: "Banana") + banana = described_class.create!(color: "Red", name: "Banana") banana.update!(color: "Yellow") expect(banana).to have_a_version_with_changes(color: "Yellow") expect(banana).not_to have_a_version_with_changes(color: "Pink") expect(banana).not_to have_a_version_with_changes(color: "Yellow", name: "Kiwi") end end + + describe "queries of versions", versioning: true do + let!(:fruit) { described_class.create(name: "Apple", mass: 1, color: "green") } + + before do + described_class.create(name: "Pear") + fruit.update(name: "Fidget") + fruit.update(name: "Digit") + end + + it "return the fruit whose name has changed" do + result = JsonVersion.where_attribute_changes(:name).map(&:item) + expect(result).to include(fruit) + end + + it "returns the fruit whose name was Fidget" do + result = JsonVersion.where_object_changes_from({ name: "Fidget" }).map(&:item) + expect(result).to include(fruit) + end + + it "returns the fruit whose name became Digit" do + result = JsonVersion.where_object_changes_to({ name: "Digit" }).map(&:item) + expect(result).to include(fruit) + end + + it "returns the fruit where the object was named Fidget before it changed" do + result = JsonVersion.where_object({ name: "Fidget" }).map(&:item) + expect(result).to include(fruit) + end + + it "returns the fruit that changed to Fidget" do + result = JsonVersion.where_object_changes({ name: "Fidget" }).map(&:item) + expect(result).to include(fruit) + end + end end end diff --git a/spec/models/gadget_spec.rb b/spec/models/gadget_spec.rb index f71817a7..5c8bd06f 100644 --- a/spec/models/gadget_spec.rb +++ b/spec/models/gadget_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" RSpec.describe Gadget, type: :model do - let(:gadget) { Gadget.create!(name: "Wrench", brand: "Acme") } + let(:gadget) { described_class.create!(name: "Wrench", brand: "Acme") } it { is_expected.to be_versioned } @@ -35,7 +35,11 @@ RSpec.describe Gadget, type: :model do gadget.update_attribute(:updated_at, Time.current + 1) }.to(change { gadget.versions.size }.by(1)) expect( - YAML.load(gadget.versions.last.object_changes).keys + if ::YAML.respond_to?(:unsafe_load) + YAML.unsafe_load(gadget.versions.last.object_changes).keys + else + YAML.load(gadget.versions.last.object_changes).keys + end ).to eq(["updated_at"]) end end diff --git a/spec/models/joined_version_spec.rb b/spec/models/joined_version_spec.rb index d144cfea..8046f1d4 100644 --- a/spec/models/joined_version_spec.rb +++ b/spec/models/joined_version_spec.rb @@ -4,10 +4,10 @@ require "spec_helper" RSpec.describe JoinedVersion, type: :model, versioning: true do let(:widget) { Widget.create!(name: FFaker::Name.name) } - let(:version) { JoinedVersion.first } + let(:version) { described_class.first } describe "default_scope" do - it { expect(JoinedVersion.default_scopes).not_to be_empty } + it { expect(described_class.default_scopes).not_to be_empty } end describe "VersionConcern::ClassMethods" do @@ -15,19 +15,19 @@ RSpec.describe JoinedVersion, type: :model, versioning: true do describe "#subsequent" do it "does not raise error when there is a default_scope that joins" do - JoinedVersion.subsequent(version).first + described_class.subsequent(version).first end end describe "#preceding" do it "does not raise error when there is a default scope that joins" do - JoinedVersion.preceding(version).first + described_class.preceding(version).first end end describe "#between" do it "does not raise error when there is a default scope that joins" do - JoinedVersion.between(Time.current, 1.minute.from_now).first + described_class.between(Time.current, 1.minute.from_now).first end end end diff --git a/spec/models/json_version_spec.rb b/spec/models/json_version_spec.rb index b4649b21..72400e3f 100644 --- a/spec/models/json_version_spec.rb +++ b/spec/models/json_version_spec.rb @@ -5,7 +5,7 @@ require "spec_helper" # The `json_versions` table tests postgres' `json` data type. So, that # table is only created when testing against postgres. if JsonVersion.table_exists? - RSpec.describe JsonVersion, type: :model do + RSpec.describe JsonVersion, type: :model, versioning: true do it "includes the VersionConcern module" do expect(described_class).to include(PaperTrail::VersionConcern) end diff --git a/spec/models/no_object_spec.rb b/spec/models/no_object_spec.rb index 900af397..95f62492 100644 --- a/spec/models/no_object_spec.rb +++ b/spec/models/no_object_spec.rb @@ -27,7 +27,11 @@ RSpec.describe NoObject, versioning: true do # New feature: destroy populates object_changes # https://github.com/paper-trail-gem/paper_trail/pull/1123 - h = YAML.load a["object_changes"] + h = if ::YAML.respond_to?(:unsafe_load) + YAML.unsafe_load a["object_changes"] + else + YAML.load a["object_changes"] + end expect(h["id"]).to eq([n.id, nil]) expect(h["letter"]).to eq([n.letter, nil]) expect(h["created_at"][0]).to be_present @@ -38,7 +42,7 @@ RSpec.describe NoObject, versioning: true do describe "reify" do it "raises error" do - n = NoObject.create!(letter: "A") + n = described_class.create!(letter: "A") v = n.versions.last expect { v.reify }.to( raise_error( @@ -51,7 +55,7 @@ RSpec.describe NoObject, versioning: true do describe "where_object" do it "raises error" do - n = NoObject.create!(letter: "A") + n = described_class.create!(letter: "A") expect { n.versions.where_object(foo: "bar") }.to( diff --git a/spec/models/person_spec.rb b/spec/models/person_spec.rb index df47fa18..bd44f19e 100644 --- a/spec/models/person_spec.rb +++ b/spec/models/person_spec.rb @@ -9,21 +9,21 @@ require "spec_helper" RSpec.describe Person, type: :model, versioning: true do describe "#time_zone" do it "returns an ActiveSupport::TimeZone" do - person = Person.new(time_zone: "Samoa") + person = described_class.new(time_zone: "Samoa") expect(person.time_zone.class).to(eq(ActiveSupport::TimeZone)) end end context "when the model is saved" do it "version.object_changes should store long serialization of TimeZone object" do - person = Person.new(time_zone: "Samoa") + person = described_class.new(time_zone: "Samoa") person.save! len = person.versions.last.object_changes.length expect((len < 105)).to(be_truthy) end it "version.object_changes attribute should have stored the value from serializer" do - person = Person.new(time_zone: "Samoa") + person = described_class.new(time_zone: "Samoa") person.save! as_stored_in_version = HashWithIndifferentAccess[ YAML.load(person.versions.last.object_changes) @@ -34,14 +34,14 @@ RSpec.describe Person, type: :model, versioning: true do end it "version.changeset should convert attribute to original, unserialized value" do - person = Person.new(time_zone: "Samoa") + person = described_class.new(time_zone: "Samoa") person.save! unserialized_value = Person::TimeZoneSerializer.load(person.time_zone) expect(person.versions.last.changeset[:time_zone].last).to(eq(unserialized_value)) end it "record.changes (before save) returns the original, unserialized values" do - person = Person.new(time_zone: "Samoa") + person = described_class.new(time_zone: "Samoa") changes_before_save = person.changes.dup person.save! expect( @@ -50,7 +50,7 @@ RSpec.describe Person, type: :model, versioning: true do end it "version.changeset should be the same as record.changes was before the save" do - person = Person.new(time_zone: "Samoa") + person = described_class.new(time_zone: "Samoa") changes_before_save = person.changes.dup person.save! actual = person.versions.last.changeset.delete_if { |k, _v| (k.to_sym == :id) } @@ -61,7 +61,7 @@ RSpec.describe Person, type: :model, versioning: true do context "when that attribute is updated" do it "object should not store long serialization of TimeZone object" do - person = Person.new(time_zone: "Samoa") + person = described_class.new(time_zone: "Samoa") person.save! person.assign_attributes(time_zone: "Pacific Time (US & Canada)") person.save! @@ -70,7 +70,7 @@ RSpec.describe Person, type: :model, versioning: true do end it "object_changes should not store long serialization of TimeZone object" do - person = Person.new(time_zone: "Samoa") + person = described_class.new(time_zone: "Samoa") person.save! person.assign_attributes(time_zone: "Pacific Time (US & Canada)") person.save! @@ -79,7 +79,7 @@ RSpec.describe Person, type: :model, versioning: true do end it "version.object attribute should have stored value from serializer" do - person = Person.new(time_zone: "Samoa") + person = described_class.new(time_zone: "Samoa") person.save! attribute_value_before_change = person.time_zone person.assign_attributes(time_zone: "Pacific Time (US & Canada)") @@ -93,7 +93,7 @@ RSpec.describe Person, type: :model, versioning: true do end it "version.object_changes attribute should have stored value from serializer" do - person = Person.new(time_zone: "Samoa") + person = described_class.new(time_zone: "Samoa") person.save! person.assign_attributes(time_zone: "Pacific Time (US & Canada)") person.save! @@ -106,7 +106,7 @@ RSpec.describe Person, type: :model, versioning: true do end it "version.reify should convert attribute to original, unserialized value" do - person = Person.new(time_zone: "Samoa") + person = described_class.new(time_zone: "Samoa") person.save! attribute_value_before_change = person.time_zone person.assign_attributes(time_zone: "Pacific Time (US & Canada)") @@ -116,7 +116,7 @@ RSpec.describe Person, type: :model, versioning: true do end it "version.changeset should convert attribute to original, unserialized value" do - person = Person.new(time_zone: "Samoa") + person = described_class.new(time_zone: "Samoa") person.save! person.assign_attributes(time_zone: "Pacific Time (US & Canada)") person.save! @@ -125,7 +125,7 @@ RSpec.describe Person, type: :model, versioning: true do end it "record.changes (before save) returns the original, unserialized values" do - person = Person.new(time_zone: "Samoa") + person = described_class.new(time_zone: "Samoa") person.save! person.assign_attributes(time_zone: "Pacific Time (US & Canada)") changes_before_save = person.changes.dup @@ -136,7 +136,7 @@ RSpec.describe Person, type: :model, versioning: true do end it "version.changeset should be the same as record.changes was before the save" do - person = Person.new(time_zone: "Samoa") + person = described_class.new(time_zone: "Samoa") person.save! person.assign_attributes(time_zone: "Pacific Time (US & Canada)") changes_before_save = person.changes.dup @@ -151,7 +151,7 @@ RSpec.describe Person, type: :model, versioning: true do describe "#cars and bicycles" do it "can be reified" do - person = Person.create(name: "Frank") + person = described_class.create(name: "Frank") car = Car.create(name: "BMW 325") bicycle = Bicycle.create(name: "BMX 1.0") diff --git a/spec/models/pet_spec.rb b/spec/models/pet_spec.rb index 68362a7d..061e86b2 100644 --- a/spec/models/pet_spec.rb +++ b/spec/models/pet_spec.rb @@ -5,7 +5,7 @@ require "rails/generators" RSpec.describe Pet, type: :model, versioning: true do it "baseline test setup" do - expect(Pet.new).to be_versioned + expect(described_class.new).to be_versioned end it "can be reified" do @@ -13,8 +13,8 @@ RSpec.describe Pet, type: :model, versioning: true do dog = Dog.create(name: "Snoopy") cat = Cat.create(name: "Garfield") - person.pets << Pet.create(animal: dog) - person.pets << Pet.create(animal: cat) + person.pets << described_class.create(animal: dog) + person.pets << described_class.create(animal: cat) person.update(name: "Steve") dog.update(name: "Beethoven") diff --git a/spec/models/plant_spec.rb b/spec/models/plant_spec.rb index 004b6505..4656b1f3 100644 --- a/spec/models/plant_spec.rb +++ b/spec/models/plant_spec.rb @@ -4,8 +4,8 @@ require "spec_helper" RSpec.describe Plant, type: :model, versioning: true do it "baseline test setup" do - expect(Plant.new).to be_versioned - expect(Plant.inheritance_column).to eq("species") + expect(described_class.new).to be_versioned + expect(described_class.inheritance_column).to eq("species") end describe "#descends_from_active_record?" do @@ -15,14 +15,14 @@ RSpec.describe Plant, type: :model, versioning: true do end it "works with non standard STI column contents" do - plant = Plant.create + plant = described_class.create plant.destroy tomato = Tomato.create tomato.destroy reified = plant.versions.last.reify - expect(reified.class).to eq(Plant) + expect(reified.class).to eq(described_class) reified = tomato.versions.last.reify expect(reified.class).to eq(Tomato) diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index 57cec579..59bf49f4 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -5,7 +5,7 @@ require "spec_helper" # The `Post` model uses a custom version class, `PostVersion` RSpec.describe Post, type: :model, versioning: true do it "inserts records into the correct table, post_versions" do - post = Post.create + post = described_class.create expect(PostVersion.count).to(eq(1)) post.update(content: "Some new content") expect(PostVersion.count).to(eq(2)) @@ -14,20 +14,20 @@ RSpec.describe Post, type: :model, versioning: true do context "with the first version" do it "have the correct index" do - post = Post.create + post = described_class.create version = post.versions.first expect(version.index).to(eq(0)) end end it "have versions of the custom class" do - post = Post.create + post = described_class.create expect(post.versions.first.class.name).to(eq("PostVersion")) end describe "#changeset" do it "returns nil because the object_changes column doesn't exist" do - post = Post.create + post = described_class.create post.update(content: "Some new content") expect(post.versions.last.changeset).to(be_nil) end diff --git a/spec/models/skipper_spec.rb b/spec/models/skipper_spec.rb index 29e2f363..2e4189ed 100644 --- a/spec/models/skipper_spec.rb +++ b/spec/models/skipper_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Skipper, type: :model, versioning: true do let(:t2) { Time.zone.local(2015, 7, 15, 20, 34, 30) } it "does not create a version" do - skipper = Skipper.create!(another_timestamp: t1) + skipper = described_class.create!(another_timestamp: t1) expect { skipper.update!(another_timestamp: t2) }.not_to(change { skipper.versions.length }) @@ -25,28 +25,28 @@ RSpec.describe Skipper, type: :model, versioning: true do if ActiveRecord.gem_version >= Gem::Version.new("6") it "does not create a version for skipped attributes" do - skipper = Skipper.create!(another_timestamp: t1) + skipper = described_class.create!(another_timestamp: t1) expect { skipper.touch(:another_timestamp, time: t2) }.not_to(change { skipper.versions.length }) end it "does not create a version for ignored attributes" do - skipper = Skipper.create!(created_at: t1) + skipper = described_class.create!(created_at: t1) expect { skipper.touch(:created_at, time: t2) }.not_to(change { skipper.versions.length }) end else it "creates a version even for skipped attributes" do - skipper = Skipper.create!(another_timestamp: t1) + skipper = described_class.create!(another_timestamp: t1) expect { skipper.touch(:another_timestamp, time: t2) }.to(change { skipper.versions.length }) end it "creates a version even for ignored attributes" do - skipper = Skipper.create!(created_at: t1) + skipper = described_class.create!(created_at: t1) expect { skipper.touch(:created_at, time: t2) }.to(change { skipper.versions.length }) @@ -54,7 +54,7 @@ RSpec.describe Skipper, type: :model, versioning: true do end it "creates a version for non-skipped timestamps" do - skipper = Skipper.create! + skipper = described_class.create! expect { skipper.touch }.to(change { skipper.versions.length }) @@ -67,7 +67,7 @@ RSpec.describe Skipper, type: :model, versioning: true do context "without preserve (default)" do it "has no timestamp" do - skipper = Skipper.create!(another_timestamp: t1) + skipper = described_class.create!(another_timestamp: t1) skipper.update!(another_timestamp: t2, name: "Foobar") skipper = skipper.versions.last.reify expect(skipper.another_timestamp).to be(nil) @@ -76,7 +76,7 @@ RSpec.describe Skipper, type: :model, versioning: true do context "with preserve" do it "preserves its timestamp" do - skipper = Skipper.create!(another_timestamp: t1) + skipper = described_class.create!(another_timestamp: t1) skipper.update!(another_timestamp: t2, name: "Foobar") skipper = skipper.versions.last.reify(unversioned_attributes: :preserve) expect(skipper.another_timestamp).to eq(t2) diff --git a/spec/models/thing_spec.rb b/spec/models/thing_spec.rb index ac7b8521..3030abd7 100644 --- a/spec/models/thing_spec.rb +++ b/spec/models/thing_spec.rb @@ -4,10 +4,10 @@ require "spec_helper" RSpec.describe Thing, type: :model do describe "#versions", versioning: true do - let(:thing) { Thing.create! } + let(:thing) { described_class.create! } it "applies the scope option" do - expect(Thing.reflect_on_association(:versions).scope).to be_a Proc + expect(described_class.reflect_on_association(:versions).scope).to be_a Proc expect(thing.versions.to_sql).to end_with "ORDER BY id desc" end diff --git a/spec/models/translation_spec.rb b/spec/models/translation_spec.rb index 51413d5d..23bb1450 100644 --- a/spec/models/translation_spec.rb +++ b/spec/models/translation_spec.rb @@ -24,6 +24,14 @@ RSpec.describe Translation, type: :model, versioning: true do expect(PaperTrail::Version.count).to(eq(0)) end end + + context "when after touch" do + it "not change the number of versions" do + translation = described_class.create!(headline: "Headline") + translation.touch + expect(PaperTrail::Version.count).to(eq(0)) + end + end end context "with US translations" do @@ -31,7 +39,7 @@ RSpec.describe Translation, type: :model, versioning: true do it "creation does not change the number of versions" do translation = described_class.new(headline: "Headline") translation.language_code = "US" - translation.type = "DRAFT" + translation.draft_status = "DRAFT" translation.save! expect(PaperTrail::Version.count).to(eq(0)) end @@ -39,11 +47,20 @@ RSpec.describe Translation, type: :model, versioning: true do it "update does not change the number of versions" do translation = described_class.new(headline: "Headline") translation.language_code = "US" - translation.type = "DRAFT" + translation.draft_status = "DRAFT" translation.save! translation.update(content: "Content") expect(PaperTrail::Version.count).to(eq(0)) end + + it "touch does not change the number of versions" do + translation = described_class.new(headline: "Headline") + translation.language_code = "US" + translation.draft_status = "DRAFT" + translation.save! + translation.touch + expect(PaperTrail::Version.count).to(eq(0)) + end end context "with non-drafts" do @@ -52,14 +69,21 @@ RSpec.describe Translation, type: :model, versioning: true do expect(PaperTrail::Version.count).to(eq(1)) end - it "update does not change the number of versions" do + it "update changes the number of versions" do translation = described_class.create!(headline: "Headline", language_code: "US") translation.update(content: "Content") expect(PaperTrail::Version.count).to(eq(2)) expect(translation.versions.size).to(eq(2)) end - it "destroy does not change the number of versions" do + it "touch changes the number of versions" do + translation = described_class.create!(headline: "Headline", language_code: "US") + translation.touch + expect(PaperTrail::Version.count).to(eq(2)) + expect(translation.versions.size).to(eq(2)) + end + + it "destroy changes the number of versions" do translation = described_class.new(headline: "Headline") translation.language_code = "US" translation.save! diff --git a/spec/models/vegetable_spec.rb b/spec/models/vegetable_spec.rb new file mode 100644 index 00000000..7ab422a8 --- /dev/null +++ b/spec/models/vegetable_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "spec_helper" +require "support/performance_helpers" + +if ENV["DB"] == "postgres" && JsonbVersion.table_exists? + ::RSpec.describe Vegetable do + describe "queries of versions", versioning: true do + let!(:vegetable) { described_class.create(name: "Veggie", mass: 1, color: "green") } + + before do + vegetable.update(name: "Fidget") + vegetable.update(name: "Digit") + described_class.create(name: "Cucumber") + end + + it "return the vegetable whose name has changed" do + result = JsonbVersion.where_attribute_changes(:name).map(&:item) + expect(result).to include(vegetable) + end + + it "returns the vegetable whose name was Fidget" do + result = JsonbVersion.where_object_changes_from({ name: "Fidget" }).map(&:item) + expect(result).to include(vegetable) + end + + it "returns the vegetable whose name became Digit" do + result = JsonbVersion.where_object_changes_to({ name: "Digit" }).map(&:item) + expect(result).to include(vegetable) + end + + it "returns the vegetable where the object was named Fidget before it changed" do + result = JsonbVersion.where_object({ name: "Fidget" }).map(&:item) + expect(result).to include(vegetable) + end + + it "returns the vegetable that changed to Fidget" do + result = JsonbVersion.where_object_changes({ name: "Fidget" }).map(&:item) + expect(result).to include(vegetable) + end + end + end +end diff --git a/spec/models/version_spec.rb b/spec/models/version_spec.rb index 9ecfdbce..4263476b 100644 --- a/spec/models/version_spec.rb +++ b/spec/models/version_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "spec_helper" +require "support/shared_examples/queries" module PaperTrail ::RSpec.describe Version, type: :model do @@ -60,7 +61,7 @@ module PaperTrail describe "#paper_trail_originator" do context "with no previous versions" do it "returns nil" do - expect(PaperTrail::Version.new.paper_trail_originator).to be_nil + expect(described_class.new.paper_trail_originator).to be_nil end end @@ -78,7 +79,7 @@ module PaperTrail describe "#previous" do context "with no previous versions" do it "returns nil" do - expect(PaperTrail::Version.new.previous).to be_nil + expect(described_class.new.previous).to be_nil end end @@ -88,7 +89,7 @@ module PaperTrail widget = Widget.create!(name: FFaker::Name.name) widget.versions.first.update!(whodunnit: name) widget.update!(name: FFaker::Name.first_name) - expect(widget.versions.last.previous).to be_instance_of(PaperTrail::Version) + expect(widget.versions.last.previous).to be_instance_of(described_class) end end end @@ -96,442 +97,39 @@ module PaperTrail describe "#terminator" do it "is an alias for the `whodunnit` attribute" do attributes = { whodunnit: FFaker::Name.first_name } - version = PaperTrail::Version.new(attributes) + version = described_class.new(attributes) expect(version.terminator).to eq(attributes[:whodunnit]) end end describe "#version_author" do it "is an alias for the `terminator` method" do - version = PaperTrail::Version.new + version = described_class.new expect(version.method(:version_author)).to eq(version.method(:terminator)) end end - context "when changing the data type of database columns on the fly" do - # TODO: Changing the data type of these database columns in the middle - # of the test suite adds a fair amount of complexity. Is there a better - # way? We already have a `json_versions` table in our tests, maybe we - # could use that and add a `jsonb_versions` table? - column_overrides = [false] - if ENV["DB"] == "postgres" - column_overrides += %w[json jsonb] + context "with text columns", versioning: true do + include_examples "queries", :text, ::Widget, :an_integer + end + + if ENV["DB"] == "postgres" + context "with json columns", versioning: true do + include_examples( + "queries", + :json, + ::Fruit, # uses JsonVersion + :mass + ) end - column_overrides.shuffle.each do |column_datatype_override| - context "with a #{column_datatype_override || 'text'} column" do - let(:widget) { Widget.new } - let(:name) { FFaker::Name.first_name } - let(:int) { column_datatype_override ? 1 : rand(2..6) } - - before do - if column_datatype_override - ActiveRecord::Base.connection.execute("SAVEPOINT pgtest;") - %w[object object_changes].each do |column| - ActiveRecord::Base.connection.execute( - "ALTER TABLE versions DROP COLUMN #{column};" - ) - ActiveRecord::Base.connection.execute( - "ALTER TABLE versions ADD COLUMN #{column} #{column_datatype_override};" - ) - end - PaperTrail::Version.reset_column_information - end - end - - after do - PaperTrail.serializer = PaperTrail::Serializers::YAML - - if column_datatype_override - ActiveRecord::Base.connection.execute("ROLLBACK TO SAVEPOINT pgtest;") - PaperTrail::Version.reset_column_information - end - end - - describe "#where_attribute_changes", versioning: true do - it "requires its argument to be a string or a symbol" do - expect { - PaperTrail::Version.where_attribute_changes({}) - }.to raise_error(ArgumentError) - expect { - PaperTrail::Version.where_attribute_changes([]) - }.to raise_error(ArgumentError) - end - - context "with object_changes_adapter configured" do - after do - PaperTrail.config.object_changes_adapter = nil - end - - it "calls the adapter's where_attribute_changes method" do - adapter = instance_spy("CustomObjectChangesAdapter") - bicycle = Bicycle.create!(name: "abc") - bicycle.update!(name: "xyz") - - allow(adapter).to( - receive(:where_attribute_changes).with(Version, :name) - ).and_return([bicycle.versions[0], bicycle.versions[1]]) - - PaperTrail.config.object_changes_adapter = adapter - expect( - bicycle.versions.where_attribute_changes(:name) - ).to match_array([bicycle.versions[0], bicycle.versions[1]]) - expect(adapter).to have_received(:where_attribute_changes) - end - - it "defaults to the original behavior" do - adapter = Class.new.new - PaperTrail.config.object_changes_adapter = adapter - bicycle = Bicycle.create!(name: "abc") - bicycle.update!(name: "xyz") - - if column_datatype_override - expect( - bicycle.versions.where_attribute_changes(:name) - ).to match_array([bicycle.versions[0], bicycle.versions[1]]) - else - expect { - bicycle.versions.where_attribute_changes(:name) - }.to raise_error( - UnsupportedColumnType, - "where_attribute_changes expected json or jsonb column, got text" - ) - end - end - end - - # Only test json and jsonb columns. where_attribute_changes does - # not support text columns. - if column_datatype_override - it "locates versions according to their object_changes contents" do - widget.update!(name: "foobar", an_integer: 100) - widget.update!(an_integer: 17) - - expect( - widget.versions.where_attribute_changes(:name) - ).to eq([widget.versions[0]]) - expect( - widget.versions.where_attribute_changes("an_integer") - ).to eq([widget.versions[0], widget.versions[1]]) - expect( - widget.versions.where_attribute_changes(:a_float) - ).to eq([]) - end - else - it "raises error" do - expect { - widget.versions.where_attribute_changes(:name).to_a - }.to raise_error( - UnsupportedColumnType, - "where_attribute_changes expected json or jsonb column, got text" - ) - end - end - end - - describe "#where_object", versioning: true do - it "requires its argument to be a Hash" do - widget.update!(name: name, an_integer: int) - widget.update!(name: "foobar", an_integer: 100) - widget.update!(name: FFaker::Name.last_name, an_integer: 15) - expect { - PaperTrail::Version.where_object(:foo) - }.to raise_error(ArgumentError) - expect { - PaperTrail::Version.where_object([]) - }.to raise_error(ArgumentError) - end - - context "with YAML serializer" do - it "locates versions according to their `object` contents" do - expect(PaperTrail.serializer).to be PaperTrail::Serializers::YAML - widget.update!(name: name, an_integer: int) - widget.update!(name: "foobar", an_integer: 100) - widget.update!(name: FFaker::Name.last_name, an_integer: 15) - expect( - PaperTrail::Version.where_object(an_integer: int) - ).to eq([widget.versions[1]]) - expect( - PaperTrail::Version.where_object(name: name) - ).to eq([widget.versions[1]]) - expect( - PaperTrail::Version.where_object(an_integer: 100) - ).to eq([widget.versions[2]]) - end - end - - context "with JSON serializer" do - it "locates versions according to their `object` contents" do - PaperTrail.serializer = PaperTrail::Serializers::JSON - expect(PaperTrail.serializer).to be PaperTrail::Serializers::JSON - widget.update!(name: name, an_integer: int) - widget.update!(name: "foobar", an_integer: 100) - widget.update!(name: FFaker::Name.last_name, an_integer: 15) - expect( - PaperTrail::Version.where_object(an_integer: int) - ).to eq([widget.versions[1]]) - expect( - PaperTrail::Version.where_object(name: name) - ).to eq([widget.versions[1]]) - expect( - PaperTrail::Version.where_object(an_integer: 100) - ).to eq([widget.versions[2]]) - end - end - end - - describe "#where_object_changes", versioning: true do - it "requires its argument to be a Hash" do - expect { - PaperTrail::Version.where_object_changes(:foo) - }.to raise_error(ArgumentError) - expect { - PaperTrail::Version.where_object_changes([]) - }.to raise_error(ArgumentError) - end - - context "with object_changes_adapter configured" do - after do - PaperTrail.config.object_changes_adapter = nil - end - - it "calls the adapter's where_object_changes method" do - adapter = instance_spy("CustomObjectChangesAdapter") - bicycle = Bicycle.create!(name: "abc") - allow(adapter).to( - receive(:where_object_changes).with(Version, name: "abc") - ).and_return(bicycle.versions[0..1]) - PaperTrail.config.object_changes_adapter = adapter - expect( - bicycle.versions.where_object_changes(name: "abc") - ).to match_array(bicycle.versions[0..1]) - expect(adapter).to have_received(:where_object_changes) - end - - it "defaults to the original behavior" do - adapter = Class.new.new - PaperTrail.config.object_changes_adapter = adapter - bicycle = Bicycle.create!(name: "abc") - if column_datatype_override - expect( - bicycle.versions.where_object_changes(name: "abc") - ).to match_array(bicycle.versions[0..1]) - else - expect { - bicycle.versions.where_object_changes(name: "abc") - }.to raise_error( - UnsupportedColumnType, - "where_object_changes expected json or jsonb column, got text" - ) - end - end - end - - # Only test json and jsonb columns. where_object_changes no longer - # supports text columns. - if column_datatype_override - it "locates versions according to their object_changes contents" do - widget.update!(name: name, an_integer: 0) - widget.update!(name: "foobar", an_integer: 100) - widget.update!(name: FFaker::Name.last_name, an_integer: int) - expect( - widget.versions.where_object_changes(name: name) - ).to eq(widget.versions[0..1]) - expect( - widget.versions.where_object_changes(an_integer: 100) - ).to eq(widget.versions[1..2]) - expect( - widget.versions.where_object_changes(an_integer: int) - ).to eq([widget.versions.last]) - expect( - widget.versions.where_object_changes(an_integer: 100, name: "foobar") - ).to eq(widget.versions[1..2]) - end - else - it "raises error" do - expect { - widget.versions.where_object_changes(name: "foo").to_a - }.to raise_error( - UnsupportedColumnType, - "where_object_changes expected json or jsonb column, got text" - ) - end - end - end - - describe "#where_object_changes_from", versioning: true do - it "requires its argument to be a Hash" do - expect { - PaperTrail::Version.where_object_changes_from(:foo) - }.to raise_error(ArgumentError) - expect { - PaperTrail::Version.where_object_changes_from([]) - }.to raise_error(ArgumentError) - end - - context "with object_changes_adapter configured" do - after do - PaperTrail.config.object_changes_adapter = nil - end - - it "calls the adapter's where_object_changes_from method" do - adapter = instance_spy("CustomObjectChangesAdapter") - bicycle = Bicycle.create!(name: "abc") - bicycle.update!(name: "xyz") - - allow(adapter).to( - receive(:where_object_changes_from).with(Version, name: "abc") - ).and_return([bicycle.versions[1]]) - - PaperTrail.config.object_changes_adapter = adapter - expect( - bicycle.versions.where_object_changes_from(name: "abc") - ).to match_array([bicycle.versions[1]]) - expect(adapter).to have_received(:where_object_changes_from) - end - - it "defaults to the original behavior" do - adapter = Class.new.new - PaperTrail.config.object_changes_adapter = adapter - bicycle = Bicycle.create!(name: "abc") - bicycle.update!(name: "xyz") - - if column_datatype_override - expect( - bicycle.versions.where_object_changes_from(name: "abc") - ).to match_array([bicycle.versions[1]]) - else - expect { - bicycle.versions.where_object_changes_from(name: "abc") - }.to raise_error( - UnsupportedColumnType, - "where_object_changes_from expected json or jsonb column, got text" - ) - end - end - end - - # Only test json and jsonb columns. where_object_changes_from does - # not support text columns. - if column_datatype_override - it "locates versions according to their object_changes contents" do - widget.update!(name: name, an_integer: 0) - widget.update!(name: "foobar", an_integer: 100) - widget.update!(name: FFaker::Name.last_name, an_integer: int) - - expect( - widget.versions.where_object_changes_from(name: name) - ).to eq([widget.versions[1]]) - expect( - widget.versions.where_object_changes_from(an_integer: 100) - ).to eq([widget.versions[2]]) - expect( - widget.versions.where_object_changes_from(an_integer: int) - ).to eq([]) - expect( - widget.versions.where_object_changes_from(an_integer: 100, name: "foobar") - ).to eq([widget.versions[2]]) - end - else - it "raises error" do - expect { - widget.versions.where_object_changes_from(name: "foo").to_a - }.to raise_error( - UnsupportedColumnType, - "where_object_changes_from expected json or jsonb column, got text" - ) - end - end - end - - describe "#where_object_changes_to", versioning: true do - it "requires its argument to be a Hash" do - expect { - PaperTrail::Version.where_object_changes_to(:foo) - }.to raise_error(ArgumentError) - expect { - PaperTrail::Version.where_object_changes_to([]) - }.to raise_error(ArgumentError) - end - - context "with object_changes_adapter configured" do - after do - PaperTrail.config.object_changes_adapter = nil - end - - it "calls the adapter's where_object_changes_to method" do - adapter = instance_spy("CustomObjectChangesAdapter") - bicycle = Bicycle.create!(name: "abc") - bicycle.update!(name: "xyz") - - allow(adapter).to( - receive(:where_object_changes_to).with(Version, name: "xyz") - ).and_return([bicycle.versions[1]]) - - PaperTrail.config.object_changes_adapter = adapter - expect( - bicycle.versions.where_object_changes_to(name: "xyz") - ).to match_array([bicycle.versions[1]]) - expect(adapter).to have_received(:where_object_changes_to) - end - - it "defaults to the original behavior" do - adapter = Class.new.new - PaperTrail.config.object_changes_adapter = adapter - bicycle = Bicycle.create!(name: "abc") - bicycle.update!(name: "xyz") - - if column_datatype_override - expect( - bicycle.versions.where_object_changes_to(name: "xyz") - ).to match_array([bicycle.versions[1]]) - else - expect { - bicycle.versions.where_object_changes_to(name: "xyz") - }.to raise_error( - UnsupportedColumnType, - "where_object_changes_to expected json or jsonb column, got text" - ) - end - end - end - - # Only test json and jsonb columns. where_object_changes_to does - # not support text columns. - if column_datatype_override - it "locates versions according to their object_changes contents" do - widget.update!(name: name, an_integer: 0) - widget.update!(name: "foobar", an_integer: 100) - widget.update!(name: FFaker::Name.last_name, an_integer: int) - - expect( - widget.versions.where_object_changes_to(name: name) - ).to eq([widget.versions[0]]) - expect( - widget.versions.where_object_changes_to(an_integer: 100) - ).to eq([widget.versions[1]]) - expect( - widget.versions.where_object_changes_to(an_integer: int) - ).to eq([widget.versions[2]]) - expect( - widget.versions.where_object_changes_to(an_integer: 100, name: "foobar") - ).to eq([widget.versions[1]]) - expect( - widget.versions.where_object_changes_to(an_integer: -1) - ).to eq([]) - end - else - it "raises error" do - expect { - widget.versions.where_object_changes_to(name: "foo").to_a - }.to raise_error( - UnsupportedColumnType, - "where_object_changes_to expected json or jsonb column, got text" - ) - end - end - end - end + context "with jsonb columns", versioning: true do + include_examples( + "queries", + :jsonb, + ::Vegetable, # uses JsonbVersion + :mass + ) end end end diff --git a/spec/models/widget_spec.rb b/spec/models/widget_spec.rb index 56b86b04..72e2393a 100644 --- a/spec/models/widget_spec.rb +++ b/spec/models/widget_spec.rb @@ -6,7 +6,7 @@ require "support/performance_helpers" RSpec.describe Widget, type: :model, versioning: true do describe "#changeset" do it "has expected values" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") changeset = widget.versions.last.changeset expect(changeset["name"]).to eq([nil, "Henry"]) expect(changeset["id"]).to eq([nil, widget.id]) @@ -24,7 +24,7 @@ RSpec.describe Widget, type: :model, versioning: true do end it "calls the adapter's load_changeset method" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") adapter = instance_spy("CustomObjectChangesAdapter") PaperTrail.config.object_changes_adapter = adapter allow(adapter).to( @@ -39,7 +39,7 @@ RSpec.describe Widget, type: :model, versioning: true do it "defaults to the original behavior" do adapter = Class.new.new PaperTrail.config.object_changes_adapter = adapter - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") changeset = widget.versions.last.changeset expect(changeset[:name]).to eq([nil, "Henry"]) end @@ -48,44 +48,44 @@ RSpec.describe Widget, type: :model, versioning: true do context "with a new record" do it "not have any previous versions" do - expect(Widget.new.versions).to(eq([])) + expect(described_class.new.versions).to(eq([])) end it "be live" do - expect(Widget.new.paper_trail.live?).to(eq(true)) + expect(described_class.new.paper_trail.live?).to(eq(true)) end end context "with a persisted record" do it "have one previous version" do - widget = Widget.create(name: "Henry", created_at: (Time.current - 1.day)) + widget = described_class.create(name: "Henry", created_at: (Time.current - 1.day)) expect(widget.versions.length).to(eq(1)) end it "be nil in its previous version" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") expect(widget.versions.first.object).to(be_nil) expect(widget.versions.first.reify).to(be_nil) end it "record the correct event" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") expect(widget.versions.first.event).to(match(/create/i)) end it "be live" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") expect(widget.paper_trail.live?).to(eq(true)) end it "use the widget `updated_at` as the version's `created_at`" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") expect(widget.versions.first.created_at.to_i).to(eq(widget.updated_at.to_i)) end context "when updated without any changes" do it "to have two previous versions" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") widget.touch expect(widget.versions.length).to eq(2) end @@ -93,13 +93,13 @@ RSpec.describe Widget, type: :model, versioning: true do context "when updated with changes" do it "have three previous versions" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") widget.update(name: "Harry") expect(widget.versions.length).to(eq(2)) end it "be available in its previous version" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") widget.update(name: "Harry") expect(widget.name).to(eq("Harry")) expect(widget.versions.last.object).not_to(be_nil) @@ -109,19 +109,19 @@ RSpec.describe Widget, type: :model, versioning: true do end it "have the same ID in its previous version" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") widget.update(name: "Harry") expect(widget.versions.last.reify.id).to(eq(widget.id)) end it "record the correct event" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") widget.update(name: "Harry") expect(widget.versions.last.event).to(match(/update/i)) end it "have versions that are not live" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") widget.update(name: "Harry") widget.versions.map(&:reify).compact.each do |v| expect(v.paper_trail).not_to be_live @@ -129,7 +129,7 @@ RSpec.describe Widget, type: :model, versioning: true do end it "have stored changes" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") widget.update(name: "Harry") last_obj_changes = widget.versions.last.object_changes actual = PaperTrail.serializer.load(last_obj_changes).reject do |k, _v| @@ -141,7 +141,7 @@ RSpec.describe Widget, type: :model, versioning: true do end it "return changes with indifferent access" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") widget.update(name: "Harry") expect(widget.versions.last.changeset[:name]).to(eq(%w[Henry Harry])) expect(widget.versions.last.changeset["name"]).to(eq(%w[Henry Harry])) @@ -150,7 +150,7 @@ RSpec.describe Widget, type: :model, versioning: true do context "when updated, and has one associated object" do it "not copy the has_one association by default when reifying" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") widget.update(name: "Harry") wotsit = widget.create_wotsit name: "John" reified_widget = widget.versions.last.reify @@ -161,7 +161,7 @@ RSpec.describe Widget, type: :model, versioning: true do context "when updated, and has many associated objects" do it "copy the has_many associations when reifying" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") widget.update(name: "Harry") widget.fluxors.create(name: "f-zero") widget.fluxors.create(name: "f-one") @@ -175,7 +175,7 @@ RSpec.describe Widget, type: :model, versioning: true do context "when updated, and has many associated polymorphic objects" do it "copy the has_many associations when reifying" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") widget.update(name: "Harry") widget.whatchamajiggers.create(name: "f-zero") widget.whatchamajiggers.create(name: "f-zero") @@ -189,7 +189,7 @@ RSpec.describe Widget, type: :model, versioning: true do context "when updated, polymorphic objects by themselves" do it "not fail with a nil pointer on the polymorphic association" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") widget.update(name: "Harry") widget = Whatchamajigger.new(name: "f-zero") widget.save! @@ -198,21 +198,21 @@ RSpec.describe Widget, type: :model, versioning: true do context "when updated, and then destroyed" do it "record the correct event" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") widget.update(name: "Harry") widget.destroy expect(PaperTrail::Version.last.event).to(match(/destroy/i)) end it "have three previous versions" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") widget.update(name: "Harry") widget.destroy expect(PaperTrail::Version.with_item_keys("Widget", widget.id).length).to(eq(3)) end it "returns the expected attributes for the reified widget" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") widget.update(name: "Harry") widget.destroy reified_widget = PaperTrail::Version.last.reify @@ -239,7 +239,7 @@ RSpec.describe Widget, type: :model, versioning: true do end it "be re-creatable from its previous version" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") widget.update(name: "Harry") widget.destroy reified_widget = PaperTrail::Version.last.reify @@ -247,7 +247,7 @@ RSpec.describe Widget, type: :model, versioning: true do end it "restore its associations on its previous version" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") widget.update(name: "Harry") widget.fluxors.create(name: "flux") widget.destroy @@ -257,9 +257,11 @@ RSpec.describe Widget, type: :model, versioning: true do end it "have nil item for last version" do - widget = Widget.create(name: "Henry") + widget = described_class.create(name: "Henry") widget.update(name: "Harry") widget.destroy + expect(widget.versions.first.item.object_id).not_to eq(widget.object_id) + expect(widget.versions.last.item.object_id).not_to eq(widget.object_id) expect(widget.versions.last.item).to be_nil end end @@ -270,7 +272,7 @@ RSpec.describe Widget, type: :model, versioning: true do let!(:t0) { Time.current } let(:previous_widget) { widget.versions.last.reify } let(:widget) { - Widget.create( + described_class.create( name: "Warble", a_text: "The quick brown fox", an_integer: 42, @@ -338,7 +340,7 @@ RSpec.describe Widget, type: :model, versioning: true do let(:last_version) { widget.versions.last } it "reify previous version" do - assert_kind_of(Widget, last_version.reify) + assert_kind_of(described_class, last_version.reify) end it "restore all forward-compatible attributes" do @@ -362,7 +364,7 @@ RSpec.describe Widget, type: :model, versioning: true do after { PaperTrail.enabled = true } it "not add to its trail" do - widget = Widget.create(name: "Zaphod") + widget = described_class.create(name: "Zaphod") PaperTrail.enabled = false count = widget.versions.length widget.update(name: "Beeblebrox") @@ -372,23 +374,23 @@ RSpec.describe Widget, type: :model, versioning: true do context "with its paper trail turned off, when updated" do after do - PaperTrail.request.enable_model(Widget) + PaperTrail.request.enable_model(described_class) end it "not add to its trail" do - widget = Widget.create(name: "Zaphod") - PaperTrail.request.disable_model(Widget) + widget = described_class.create(name: "Zaphod") + PaperTrail.request.disable_model(described_class) count = widget.versions.length widget.update(name: "Beeblebrox") expect(widget.versions.length).to(eq(count)) end it "add to its trail" do - widget = Widget.create(name: "Zaphod") - PaperTrail.request.disable_model(Widget) + widget = described_class.create(name: "Zaphod") + PaperTrail.request.disable_model(described_class) count = widget.versions.length widget.update(name: "Beeblebrox") - PaperTrail.request.enable_model(Widget) + PaperTrail.request.enable_model(described_class) widget.update(name: "Ford") expect(widget.versions.length).to(eq((count + 1))) end @@ -398,7 +400,7 @@ RSpec.describe Widget, type: :model, versioning: true do context "with somebody making changes" do context "when a record is created" do it "tracks who made the change" do - widget = Widget.new(name: "Fidget") + widget = described_class.new(name: "Fidget") PaperTrail.request.whodunnit = "Alice" widget.save version = widget.versions.last @@ -411,7 +413,7 @@ RSpec.describe Widget, type: :model, versioning: true do context "when created, then updated" do it "tracks who made the change" do - widget = Widget.new(name: "Fidget") + widget = described_class.new(name: "Fidget") PaperTrail.request.whodunnit = "Alice" widget.save PaperTrail.request.whodunnit = "Bob" @@ -426,7 +428,7 @@ RSpec.describe Widget, type: :model, versioning: true do context "when created, updated, and destroyed" do it "tracks who made the change" do - widget = Widget.new(name: "Fidget") + widget = described_class.new(name: "Fidget") PaperTrail.request.whodunnit = "Alice" widget.save PaperTrail.request.whodunnit = "Bob" @@ -444,7 +446,7 @@ RSpec.describe Widget, type: :model, versioning: true do context "with an item with versions" do context "when the versions were created over time" do - let(:widget) { Widget.create(name: "Widget") } + let(:widget) { described_class.create(name: "Widget") } let(:t0) { 2.days.ago } let(:t1) { 1.day.ago } let(:t2) { 1.hour.ago } @@ -501,7 +503,7 @@ RSpec.describe Widget, type: :model, versioning: true do describe ".versions_between" do it "return versions in the time period" do - widget = Widget.create(name: "Widget") + widget = described_class.create(name: "Widget") widget.update(name: "Fidget") widget.update(name: "Digit") widget.versions[0].update(created_at: 30.days.ago) @@ -524,11 +526,11 @@ RSpec.describe Widget, type: :model, versioning: true do end context "with the first version" do - let(:widget) { Widget.create(name: "Widget") } + let(:widget) { described_class.create(name: "Widget") } let(:version) { widget.versions.last } before do - widget = Widget.create(name: "Widget") + widget = described_class.create(name: "Widget") widget.update(name: "Fidget") widget.update(name: "Digit") end @@ -547,7 +549,7 @@ RSpec.describe Widget, type: :model, versioning: true do end context "with the last version" do - let(:widget) { Widget.create(name: "Widget") } + let(:widget) { described_class.create(name: "Widget") } let(:version) { widget.versions.last } before do @@ -571,7 +573,7 @@ RSpec.describe Widget, type: :model, versioning: true do context "with a reified item" do it "know which version it came from, and return its previous self" do - widget = Widget.create(name: "Bob") + widget = described_class.create(name: "Bob") %w[Tom Dick Jane].each do |name| widget.update(name: name) end @@ -585,7 +587,7 @@ RSpec.describe Widget, type: :model, versioning: true do describe "#next_version" do context "with a reified item" do it "returns the object (not a Version) as it became next" do - widget = Widget.create(name: "Bob") + widget = described_class.create(name: "Bob") %w[Tom Dick Jane].each do |name| widget.update(name: name) end @@ -598,7 +600,7 @@ RSpec.describe Widget, type: :model, versioning: true do context "with a non-reified item" do it "always returns nil because cannot ever have a next version" do - widget = Widget.new + widget = described_class.new expect(widget.paper_trail.next_version).to(be_nil) widget.save %w[Tom Dick Jane].each do |name| @@ -612,7 +614,7 @@ RSpec.describe Widget, type: :model, versioning: true do describe "#previous_version" do context "with a reified item" do it "returns the object (not a Version) as it was most recently" do - widget = Widget.create(name: "Bob") + widget = described_class.create(name: "Bob") %w[Tom Dick Jane].each do |name| widget.update(name: name) end @@ -625,7 +627,7 @@ RSpec.describe Widget, type: :model, versioning: true do context "with a non-reified item" do it "returns the object (not a Version) as it was most recently" do - widget = Widget.new + widget = described_class.new expect(widget.paper_trail.previous_version).to(be_nil) widget.save %w[Tom Dick Jane].each do |name| @@ -638,7 +640,7 @@ RSpec.describe Widget, type: :model, versioning: true do context "with an unsaved record" do it "not have a version created on destroy" do - widget = Widget.new + widget = described_class.new widget.destroy expect(widget.versions.empty?).to(eq(true)) end @@ -646,7 +648,7 @@ RSpec.describe Widget, type: :model, versioning: true do context "when measuring the memory allocation of" do let(:widget) do - Widget.new( + described_class.new( name: "Warble", a_text: "The quick brown fox", an_integer: 42, @@ -725,7 +727,7 @@ RSpec.describe Widget, type: :model, versioning: true do end describe "`have_a_version_with` matcher", versioning: true do - let(:widget) { Widget.create! name: "Bob", an_integer: 1 } + let(:widget) { described_class.create! name: "Bob", an_integer: 1 } before do widget.update!(name: "Leonard", an_integer: 1) @@ -743,21 +745,21 @@ RSpec.describe Widget, type: :model, versioning: true do describe "versioning option" do context "when enabled", versioning: true do it "enables versioning" do - widget = Widget.create! name: "Bob", an_integer: 1 + widget = described_class.create! name: "Bob", an_integer: 1 expect(widget.versions.size).to eq(1) end end context "when disabled", versioning: false do it "does not enable versioning" do - widget = Widget.create! name: "Bob", an_integer: 1 + widget = described_class.create! name: "Bob", an_integer: 1 expect(widget.versions.size).to eq(0) end end end describe "Callbacks", versioning: true do - let(:widget) { Widget.create! name: "Bob", an_integer: 1 } + let(:widget) { described_class.create! name: "Bob", an_integer: 1 } describe "before_save" do it "resets value for timestamp attrs for update so that value gets updated properly" do @@ -768,7 +770,7 @@ RSpec.describe Widget, type: :model, versioning: true do end describe "after_create" do - let(:widget) { Widget.create!(name: "Foobar", created_at: Time.current - 1.week) } + let(:widget) { described_class.create!(name: "Foobar", created_at: Time.current - 1.week) } it "corresponding version uses the widget's `updated_at`" do expect(widget.versions.last.created_at.to_i).to eq(widget.updated_at.to_i) @@ -832,7 +834,7 @@ RSpec.describe Widget, type: :model, versioning: true do end describe "Association", versioning: true do - let(:widget) { Widget.create! name: "Bob", an_integer: 1 } + let(:widget) { described_class.create! name: "Bob", an_integer: 1 } describe "sort order" do it "sorts by the timestamp order from the `VersionConcern`" do @@ -844,19 +846,19 @@ RSpec.describe Widget, type: :model, versioning: true do end describe "#create", versioning: true do - let(:widget) { Widget.create! name: "Bob", an_integer: 1 } + let(:widget) { described_class.create! name: "Bob", an_integer: 1 } it "creates a version record" do - wordget = Widget.create + wordget = described_class.create assert_equal 1, wordget.versions.length end end describe "#destroy", versioning: true do - let(:widget) { Widget.create! name: "Bob", an_integer: 1 } + let(:widget) { described_class.create! name: "Bob", an_integer: 1 } it "creates a version record" do - widget = Widget.create + widget = described_class.create assert_equal 1, widget.versions.length widget.destroy versions_for_widget = PaperTrail::Version.with_item_keys("Widget", widget.id) @@ -869,7 +871,7 @@ RSpec.describe Widget, type: :model, versioning: true do # the `widget.versions` association, instead of `with_item_keys`. PaperTrail::Version.with_item_keys("Widget", widget.id) } - widget = Widget.create + widget = described_class.create assert_equal 1, widget.versions.length widget.destroy assert_equal 2, versions.call(widget).length @@ -883,7 +885,7 @@ RSpec.describe Widget, type: :model, versioning: true do end describe "#paper_trail.originator", versioning: true do - let(:widget) { Widget.create! name: "Bob", an_integer: 1 } + let(:widget) { described_class.create! name: "Bob", an_integer: 1 } describe "return value" do let(:orig_name) { FFaker::Name.name } @@ -923,7 +925,7 @@ RSpec.describe Widget, type: :model, versioning: true do end describe "#version_at", versioning: true do - let(:widget) { Widget.create! name: "Bob", an_integer: 1 } + let(:widget) { described_class.create! name: "Bob", an_integer: 1 } context "when Timestamp argument is AFTER object has been destroyed" do it "returns nil" do @@ -935,7 +937,7 @@ RSpec.describe Widget, type: :model, versioning: true do end describe "touch", versioning: true do - let(:widget) { Widget.create! name: "Bob", an_integer: 1 } + let(:widget) { described_class.create! name: "Bob", an_integer: 1 } it "creates a version" do expect { widget.touch }.to change { @@ -953,10 +955,10 @@ RSpec.describe Widget, type: :model, versioning: true do end describe ".paper_trail.update_columns", versioning: true do - let(:widget) { Widget.create! name: "Bob", an_integer: 1 } + let(:widget) { described_class.create! name: "Bob", an_integer: 1 } it "creates a version record" do - widget = Widget.create + widget = described_class.create expect(widget.versions.count).to eq(1) widget.paper_trail.update_columns(name: "Bugle") expect(widget.versions.count).to eq(2) @@ -966,10 +968,10 @@ RSpec.describe Widget, type: :model, versioning: true do end describe "#update", versioning: true do - let(:widget) { Widget.create! name: "Bob", an_integer: 1 } + let(:widget) { described_class.create! name: "Bob", an_integer: 1 } it "creates a version record" do - widget = Widget.create + widget = described_class.create assert_equal 1, widget.versions.length widget.update(name: "Bugle") assert_equal 2, widget.versions.length diff --git a/spec/models/wotsit_spec.rb b/spec/models/wotsit_spec.rb index 7f12eb0b..e72fbefa 100644 --- a/spec/models/wotsit_spec.rb +++ b/spec/models/wotsit_spec.rb @@ -4,7 +4,7 @@ require "spec_helper" RSpec.describe Wotsit, versioning: true do it "update! records timestamps" do - wotsit = Wotsit.create!(name: "wotsit") + wotsit = described_class.create!(name: "wotsit") wotsit.update!(name: "changed") reified = wotsit.versions.last.reify expect(reified.created_at).not_to(be_nil) @@ -12,7 +12,7 @@ RSpec.describe Wotsit, versioning: true do end it "update! does not raise error" do - wotsit = Wotsit.create!(name: "name1") + wotsit = described_class.create!(name: "name1") expect { wotsit.update!(name: "name2") }.not_to(raise_error) end end diff --git a/spec/paper_trail/compatibility_spec.rb b/spec/paper_trail/compatibility_spec.rb index dae02398..79c65976 100644 --- a/spec/paper_trail/compatibility_spec.rb +++ b/spec/paper_trail/compatibility_spec.rb @@ -14,7 +14,7 @@ module PaperTrail context "when incompatible" do it "writes a warning to stderr" do - ar_version = ::Gem::Version.new("7.0.0") + ar_version = ::Gem::Version.new("7.1.0") expect { described_class.check_activerecord(ar_version) }.to output(/not compatible/).to_stderr diff --git a/spec/paper_trail/events/base_spec.rb b/spec/paper_trail/events/base_spec.rb index da7e1e62..fd504cec 100644 --- a/spec/paper_trail/events/base_spec.rb +++ b/spec/paper_trail/events/base_spec.rb @@ -9,7 +9,7 @@ module PaperTrail context "with a new record" do it "returns true" do g = Gadget.new(created_at: Time.current) - event = PaperTrail::Events::Base.new(g, false) + event = described_class.new(g, false) expect(event.changed_notably?).to eq(true) end end @@ -18,14 +18,14 @@ module PaperTrail it "only acknowledges non-ignored attrs" do gadget = Gadget.create!(created_at: Time.current) gadget.name = "Wrench" - event = PaperTrail::Events::Base.new(gadget, false) + event = described_class.new(gadget, false) expect(event.changed_notably?).to eq(true) end it "does not acknowledge ignored attr (brand)" do gadget = Gadget.create!(created_at: Time.current) gadget.brand = "Acme" - event = PaperTrail::Events::Base.new(gadget, false) + event = described_class.new(gadget, false) expect(event.changed_notably?).to eq(false) end end @@ -35,7 +35,7 @@ module PaperTrail gadget = Gadget.create!(created_at: Time.current) gadget.name = "Wrench" gadget.updated_at = Time.current - event = PaperTrail::Events::Base.new(gadget, false) + event = described_class.new(gadget, false) expect(event.changed_notably?).to eq(true) end @@ -43,7 +43,7 @@ module PaperTrail gadget = Gadget.create!(created_at: Time.current) gadget.brand = "Acme" gadget.updated_at = Time.current - event = PaperTrail::Events::Base.new(gadget, false) + event = described_class.new(gadget, false) expect(event.changed_notably?).to eq(false) end end @@ -53,7 +53,7 @@ module PaperTrail it "returns a hash lacking the skipped attribute" do # Skipper has_paper_trail(..., skip: [:another_timestamp]) skipper = Skipper.create!(another_timestamp: Time.current) - event = PaperTrail::Events::Base.new(skipper, false) + event = described_class.new(skipper, false) attributes = event.send(:nonskipped_attributes_before_change, false) expect(attributes).not_to have_key("another_timestamp") end diff --git a/spec/paper_trail/events/destroy_spec.rb b/spec/paper_trail/events/destroy_spec.rb index c63accdd..f75d3ef5 100644 --- a/spec/paper_trail/events/destroy_spec.rb +++ b/spec/paper_trail/events/destroy_spec.rb @@ -11,17 +11,21 @@ module PaperTrail name: "Carter", path_to_stardom: "Mexican radio" ) - data = PaperTrail::Events::Destroy.new(carter, true).data + data = described_class.new(carter, true).data expect(data[:item_type]).to eq("Family::Family") expect(data[:item_subtype]).to eq("Family::CelebrityFamily") end context "with skipper" do let(:skipper) { Skipper.create!(another_timestamp: Time.current) } - let(:data) { PaperTrail::Events::Destroy.new(skipper, false).data } + let(:data) { described_class.new(skipper, false).data } it "includes `object` without skipped attributes" do - object = YAML.load(data[:object]) + object = if ::YAML.respond_to?(:unsafe_load) + YAML.unsafe_load(data[:object]) + else + YAML.load(data[:object]) + end expect(object["id"]).to eq(skipper.id) expect(object).to have_key("updated_at") expect(object).to have_key("created_at") @@ -29,7 +33,11 @@ module PaperTrail end it "includes `object_changes` without skipped and ignored attributes" do - changes = YAML.load(data[:object_changes]) + changes = if ::YAML.respond_to?(:unsafe_load) + YAML.unsafe_load(data[:object_changes]) + else + YAML.load(data[:object_changes]) + end expect(changes["id"]).to eq([skipper.id, nil]) expect(changes["updated_at"][0]).to be_present expect(changes["updated_at"][1]).to be_nil diff --git a/spec/paper_trail/events/update_spec.rb b/spec/paper_trail/events/update_spec.rb index e4133ba4..06942cdf 100644 --- a/spec/paper_trail/events/update_spec.rb +++ b/spec/paper_trail/events/update_spec.rb @@ -13,7 +13,7 @@ module PaperTrail path_to_stardom: "Mexican radio" ) carter.path_to_stardom = "Johnny" - data = PaperTrail::Events::Update.new(carter, false, false, nil).data + data = described_class.new(carter, false, false, nil).data expect(data[:object_changes]).to eq( <<~YAML --- @@ -32,7 +32,7 @@ module PaperTrail path_to_stardom: "Mexican radio" ) carter.path_to_stardom = "Johnny" - data = PaperTrail::Events::Update.new(carter, false, true, nil).data + data = described_class.new(carter, false, true, nil).data expect(data[:object_changes]).to be_nil end end diff --git a/spec/paper_trail/serializers/yaml_spec.rb b/spec/paper_trail/serializers/yaml_spec.rb index 92ef85a7..94aef5c6 100644 --- a/spec/paper_trail/serializers/yaml_spec.rb +++ b/spec/paper_trail/serializers/yaml_spec.rb @@ -22,6 +22,19 @@ module PaperTrail expect(described_class.load(hash.to_yaml)).to eq(hash) expect(described_class.load(array.to_yaml)).to eq(array) end + + it "calls the expected load method based on Psych version" do + # Psych 4+ implements .unsafe_load + if ::YAML.respond_to?(:unsafe_load) + allow(::YAML).to receive(:unsafe_load) + described_class.load("string") + expect(::YAML).to have_received(:unsafe_load) + else # Psych < 4 + allow(::YAML).to receive(:load) + described_class.load("string") + expect(::YAML).to have_received(:load) + end + end end describe ".dump" do diff --git a/spec/paper_trail/type_serializers/postgres_array_serializer_spec.rb b/spec/paper_trail/type_serializers/postgres_array_serializer_spec.rb new file mode 100644 index 00000000..4bec2e84 --- /dev/null +++ b/spec/paper_trail/type_serializers/postgres_array_serializer_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require "spec_helper" + +module PaperTrail + module TypeSerializers + ::RSpec.describe PostgresArraySerializer do + let(:word_array) { [].fill(0, rand(4..8)) { ::FFaker::Lorem.word } } + let(:word_array_as_string) { word_array.join("|") } + + let(:the_thing) { described_class.new("foo", "bar") } + + describe ".deserialize" do + it "deserializes array to Ruby" do + expect(the_thing.deserialize(word_array)).to eq(word_array) + end + + it "deserializes string to Ruby array" do + allow(the_thing).to receive(:deserialize_with_ar).and_return(word_array) + expect(the_thing.deserialize(word_array_as_string)).to eq(word_array) + expect(the_thing).to have_received(:deserialize_with_ar) + end + end + + describe ".dump" do + it "serializes Ruby to JSON" do + expect(the_thing.serialize(word_array)).to eq(word_array) + end + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8cdc6a04..aaf56a92 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,7 +7,7 @@ require "simplecov" SimpleCov.start do add_filter "spec" end -SimpleCov.minimum_coverage 92.4 +SimpleCov.minimum_coverage(ENV["DB"] == "postgres" ? 97.3 : 92.4) require "byebug" require_relative "support/pt_arel_helpers" @@ -53,7 +53,7 @@ end # in `dummy_app/config/*`. By consolidating it here, # # - It can better be understood, and documented in one place -# - It can more closely resememble a conventional app boot. For example, loading +# - It can more closely resemble a conventional app boot. For example, loading # gems (like rspec-rails) _before_ loading the app. # First, `config/boot.rb` would add gems to $LOAD_PATH. diff --git a/spec/support/paper_trail_spec_migrator.rb b/spec/support/paper_trail_spec_migrator.rb index edd8f430..317dd381 100644 --- a/spec/support/paper_trail_spec_migrator.rb +++ b/spec/support/paper_trail_spec_migrator.rb @@ -18,7 +18,7 @@ class PaperTrailSpecMigrator @migrations_path = dummy_app_migrations_dir end - # Looks like the API for programatically running migrations will change + # Looks like the API for programmatically running migrations will change # in rails 5.2. This is an undocumented change, AFAICT. Then again, # how many people use the programmatic interface? Most people probably # just use rake. Maybe we're doing it wrong. diff --git a/spec/support/shared_examples/queries.rb b/spec/support/shared_examples/queries.rb new file mode 100644 index 00000000..7cad5bd1 --- /dev/null +++ b/spec/support/shared_examples/queries.rb @@ -0,0 +1,388 @@ +# frozen_string_literal: true + +RSpec.shared_examples "queries" do |column_type, model, name_of_integer_column| + let(:record) { model.new } + let(:name) { FFaker::Name.first_name } + let(:int) { column_type == :text ? 1 : rand(2..6) } + + after do + PaperTrail.serializer = PaperTrail::Serializers::YAML + end + + describe "#where_attribute_changes", versioning: true do + it "requires its argument to be a string or a symbol" do + expect { + model.paper_trail.version_class.where_attribute_changes({}) + }.to raise_error(ArgumentError) + expect { + model.paper_trail.version_class.where_attribute_changes([]) + }.to raise_error(ArgumentError) + end + + context "with object_changes_adapter configured" do + after do + PaperTrail.config.object_changes_adapter = nil + end + + it "calls the adapter's where_attribute_changes method" do + adapter = instance_spy("CustomObjectChangesAdapter") + bicycle = model.create!(name: "abc") + bicycle.update!(name: "xyz") + + allow(adapter).to( + receive(:where_attribute_changes).with(model.paper_trail.version_class, :name) + ).and_return([bicycle.versions[0], bicycle.versions[1]]) + + PaperTrail.config.object_changes_adapter = adapter + expect( + bicycle.versions.where_attribute_changes(:name) + ).to match_array([bicycle.versions[0], bicycle.versions[1]]) + expect(adapter).to have_received(:where_attribute_changes) + end + + it "defaults to the original behavior" do + adapter = Class.new.new + PaperTrail.config.object_changes_adapter = adapter + bicycle = model.create!(name: "abc") + bicycle.update!(name: "xyz") + + if column_type == :text + expect { + bicycle.versions.where_attribute_changes(:name) + }.to raise_error( + ::PaperTrail::UnsupportedColumnType, + "where_attribute_changes expected json or jsonb column, got text" + ) + else + expect( + bicycle.versions.where_attribute_changes(:name) + ).to match_array([bicycle.versions[0], bicycle.versions[1]]) + end + end + end + + if column_type == :text + it "raises error" do + expect { + record.versions.where_attribute_changes(:name).to_a + }.to raise_error( + ::PaperTrail::UnsupportedColumnType, + "where_attribute_changes expected json or jsonb column, got text" + ) + end + else + it "locates versions according to their object_changes contents" do + record.update!(name: "foobar", name_of_integer_column => 100) + record.update!(name_of_integer_column => 17) + + expect( + record.versions.where_attribute_changes(:name) + ).to eq([record.versions[0]]) + expect( + record.versions.where_attribute_changes(name_of_integer_column.to_s) + ).to eq([record.versions[0], record.versions[1]]) + expect(record.class.column_names).to include("color") + expect( + record.versions.where_attribute_changes(:color) + ).to eq([]) + end + end + end + + describe "#where_object", versioning: true do + it "requires its argument to be a Hash" do + record.update!(name: name, name_of_integer_column => int) + record.update!(name: "foobar", name_of_integer_column => 100) + record.update!(name: FFaker::Name.last_name, name_of_integer_column => 15) + expect { + model.paper_trail.version_class.where_object(:foo) + }.to raise_error(ArgumentError) + expect { + model.paper_trail.version_class.where_object([]) + }.to raise_error(ArgumentError) + end + + context "with YAML serializer" do + it "locates versions according to their `object` contents" do + expect(PaperTrail.serializer).to be PaperTrail::Serializers::YAML + record.update!(name: name, name_of_integer_column => int) + record.update!(name: "foobar", name_of_integer_column => 100) + record.update!(name: FFaker::Name.last_name, name_of_integer_column => 15) + expect( + model.paper_trail.version_class.where_object(name_of_integer_column => int) + ).to eq([record.versions[1]]) + expect( + model.paper_trail.version_class.where_object(name: name) + ).to eq([record.versions[1]]) + expect( + model.paper_trail.version_class.where_object(name_of_integer_column => 100) + ).to eq([record.versions[2]]) + end + end + + context "with JSON serializer" do + it "locates versions according to their `object` contents" do + PaperTrail.serializer = PaperTrail::Serializers::JSON + expect(PaperTrail.serializer).to be PaperTrail::Serializers::JSON + record.update!(name: name, name_of_integer_column => int) + record.update!(name: "foobar", name_of_integer_column => 100) + record.update!(name: FFaker::Name.last_name, name_of_integer_column => 15) + expect( + model.paper_trail.version_class.where_object(name_of_integer_column => int) + ).to eq([record.versions[1]]) + expect( + model.paper_trail.version_class.where_object(name: name) + ).to eq([record.versions[1]]) + expect( + model.paper_trail.version_class.where_object(name_of_integer_column => 100) + ).to eq([record.versions[2]]) + end + end + end + + describe "#where_object_changes", versioning: true do + it "requires its argument to be a Hash" do + expect { + model.paper_trail.version_class.where_object_changes(:foo) + }.to raise_error(ArgumentError) + expect { + model.paper_trail.version_class.where_object_changes([]) + }.to raise_error(ArgumentError) + end + + context "with object_changes_adapter configured" do + after do + PaperTrail.config.object_changes_adapter = nil + end + + it "calls the adapter's where_object_changes method" do + adapter = instance_spy("CustomObjectChangesAdapter") + bicycle = model.create!(name: "abc") + allow(adapter).to( + receive(:where_object_changes).with(model.paper_trail.version_class, name: "abc") + ).and_return(bicycle.versions[0..1]) + PaperTrail.config.object_changes_adapter = adapter + expect( + bicycle.versions.where_object_changes(name: "abc") + ).to match_array(bicycle.versions[0..1]) + expect(adapter).to have_received(:where_object_changes) + end + + it "defaults to the original behavior" do + adapter = Class.new.new + PaperTrail.config.object_changes_adapter = adapter + bicycle = model.create!(name: "abc") + if column_type == :text + expect { + bicycle.versions.where_object_changes(name: "abc") + }.to raise_error( + ::PaperTrail::UnsupportedColumnType, + "where_object_changes expected json or jsonb column, got text" + ) + else + expect( + bicycle.versions.where_object_changes(name: "abc") + ).to match_array(bicycle.versions[0..1]) + end + end + end + + if column_type == :text + it "raises error" do + expect { + record.versions.where_object_changes(name: "foo").to_a + }.to raise_error( + ::PaperTrail::UnsupportedColumnType, + "where_object_changes expected json or jsonb column, got text" + ) + end + else + it "locates versions according to their object_changes contents" do + record.update!(name: name, name_of_integer_column => 0) + record.update!(name: "foobar", name_of_integer_column => 100) + record.update!(name: FFaker::Name.last_name, name_of_integer_column => int) + expect( + record.versions.where_object_changes(name: name) + ).to eq(record.versions[0..1]) + expect( + record.versions.where_object_changes(name_of_integer_column => 100) + ).to eq(record.versions[1..2]) + expect( + record.versions.where_object_changes(name_of_integer_column => int) + ).to eq([record.versions.last]) + expect( + record.versions.where_object_changes(name_of_integer_column => 100, name: "foobar") + ).to eq(record.versions[1..2]) + end + end + end + + describe "#where_object_changes_from", versioning: true do + it "requires its argument to be a Hash" do + expect { + model.paper_trail.version_class.where_object_changes_from(:foo) + }.to raise_error(ArgumentError) + expect { + model.paper_trail.version_class.where_object_changes_from([]) + }.to raise_error(ArgumentError) + end + + context "with object_changes_adapter configured" do + after do + PaperTrail.config.object_changes_adapter = nil + end + + it "calls the adapter's where_object_changes_from method" do + adapter = instance_spy("CustomObjectChangesAdapter") + bicycle = model.create!(name: "abc") + bicycle.update!(name: "xyz") + + allow(adapter).to( + receive(:where_object_changes_from).with(model.paper_trail.version_class, name: "abc") + ).and_return([bicycle.versions[1]]) + + PaperTrail.config.object_changes_adapter = adapter + expect( + bicycle.versions.where_object_changes_from(name: "abc") + ).to match_array([bicycle.versions[1]]) + expect(adapter).to have_received(:where_object_changes_from) + end + + it "defaults to the original behavior" do + adapter = Class.new.new + PaperTrail.config.object_changes_adapter = adapter + bicycle = model.create!(name: "abc") + bicycle.update!(name: "xyz") + + if column_type == :text + expect { + bicycle.versions.where_object_changes_from(name: "abc") + }.to raise_error( + ::PaperTrail::UnsupportedColumnType, + "where_object_changes_from expected json or jsonb column, got text" + ) + else + expect( + bicycle.versions.where_object_changes_from(name: "abc") + ).to match_array([bicycle.versions[1]]) + end + end + end + + if column_type == :text + it "raises error" do + expect { + record.versions.where_object_changes_from(name: "foo").to_a + }.to raise_error( + ::PaperTrail::UnsupportedColumnType, + "where_object_changes_from expected json or jsonb column, got text" + ) + end + else + it "locates versions according to their object_changes contents" do + record.update!(name: name, name_of_integer_column => 0) + record.update!(name: "foobar", name_of_integer_column => 100) + record.update!(name: FFaker::Name.last_name, name_of_integer_column => int) + + expect( + record.versions.where_object_changes_from(name: name) + ).to eq([record.versions[1]]) + expect( + record.versions.where_object_changes_from(name_of_integer_column => 100) + ).to eq([record.versions[2]]) + expect( + record.versions.where_object_changes_from(name_of_integer_column => int) + ).to eq([]) + expect( + record.versions.where_object_changes_from(name_of_integer_column => 100, name: "foobar") + ).to eq([record.versions[2]]) + end + end + end + + describe "#where_object_changes_to", versioning: true do + it "requires its argument to be a Hash" do + expect { + model.paper_trail.version_class.where_object_changes_to(:foo) + }.to raise_error(ArgumentError) + expect { + model.paper_trail.version_class.where_object_changes_to([]) + }.to raise_error(ArgumentError) + end + + context "with object_changes_adapter configured" do + after do + PaperTrail.config.object_changes_adapter = nil + end + + it "calls the adapter's where_object_changes_to method" do + adapter = instance_spy("CustomObjectChangesAdapter") + bicycle = model.create!(name: "abc") + bicycle.update!(name: "xyz") + + allow(adapter).to( + receive(:where_object_changes_to).with(model.paper_trail.version_class, name: "xyz") + ).and_return([bicycle.versions[1]]) + + PaperTrail.config.object_changes_adapter = adapter + expect( + bicycle.versions.where_object_changes_to(name: "xyz") + ).to match_array([bicycle.versions[1]]) + expect(adapter).to have_received(:where_object_changes_to) + end + + it "defaults to the original behavior" do + adapter = Class.new.new + PaperTrail.config.object_changes_adapter = adapter + bicycle = model.create!(name: "abc") + bicycle.update!(name: "xyz") + + if column_type == :text + expect { + bicycle.versions.where_object_changes_to(name: "xyz") + }.to raise_error( + ::PaperTrail::UnsupportedColumnType, + "where_object_changes_to expected json or jsonb column, got text" + ) + else + expect( + bicycle.versions.where_object_changes_to(name: "xyz") + ).to match_array([bicycle.versions[1]]) + end + end + end + + if column_type == :text + it "raises error" do + expect { + record.versions.where_object_changes_to(name: "foo").to_a + }.to raise_error( + ::PaperTrail::UnsupportedColumnType, + "where_object_changes_to expected json or jsonb column, got text" + ) + end + else + it "locates versions according to their object_changes contents" do + record.update!(name: name, name_of_integer_column => 0) + record.update!(name: "foobar", name_of_integer_column => 100) + record.update!(name: FFaker::Name.last_name, name_of_integer_column => int) + + expect( + record.versions.where_object_changes_to(name: name) + ).to eq([record.versions[0]]) + expect( + record.versions.where_object_changes_to(name_of_integer_column => 100) + ).to eq([record.versions[1]]) + expect( + record.versions.where_object_changes_to(name_of_integer_column => int) + ).to eq([record.versions[2]]) + expect( + record.versions.where_object_changes_to(name_of_integer_column => 100, name: "foobar") + ).to eq([record.versions[1]]) + expect( + record.versions.where_object_changes_to(name_of_integer_column => -1) + ).to eq([]) + end + end + end +end