Merge pull request #1367 from paper-trail-gem/release-12.2.0

Release 12.2.0
This commit is contained in:
Jared Beck 2022-01-21 00:24:45 -05:00 committed by GitHub
commit f21e9a4368
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 975 additions and 737 deletions

View File

@ -32,11 +32,11 @@ require "bundler/inline"
# STEP ONE: What versions are you using? # STEP ONE: What versions are you using?
gemfile(true) do gemfile(true) do
ruby "2.5.1" ruby "3.0.2"
source "https://rubygems.org" source "https://rubygems.org"
gem "activerecord", "5.2.0" gem "activerecord", "6.1.4.1"
gem "minitest", "5.11.3" 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" gem "sqlite3", "1.3.13"
end end

View File

@ -14,7 +14,7 @@ jobs:
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
# See "Lowest supported ruby version" in CONTRIBUTING.md # See "Lowest supported ruby version" in CONTRIBUTING.md
ruby-version: '2.5' ruby-version: '2.6'
- name: Bundle - name: Bundle
run: | run: |
gem install bundler gem install bundler
@ -63,13 +63,15 @@ jobs:
# in case it still produces any deprecation warnings. # in case it still produces any deprecation warnings.
# #
# See "Lowest supported ruby version" in CONTRIBUTING.md # 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: exclude:
# rails 5.2 requires ruby < 3.0 # rails 5.2 requires ruby < 3.0
# https://github.com/rails/rails/issues/40938 # https://github.com/rails/rails/issues/40938
- ruby: '3.0' - ruby: '3.0'
gemfile: 'rails_5.2' gemfile: 'rails_5.2'
- ruby: '3.1'
gemfile: 'rails_5.2'
steps: steps:
- name: Checkout source - name: Checkout source
uses: actions/checkout@v2 uses: actions/checkout@v2

View File

@ -14,25 +14,24 @@ inherit_from: .rubocop_todo.yml
# - Only include permanent config; temporary goes in .rubocop_todo.yml # - Only include permanent config; temporary goes in .rubocop_todo.yml
AllCops: AllCops:
# Generated files, like schema.rb, are out of our control.
Exclude: Exclude:
- gemfiles/vendor/bundle/**/* # This dir only shows up on travis ¯\_(ツ)_/¯ - gemfiles/*
- spec/dummy_app/db/schema.rb # Generated, out of our control - spec/dummy_app/db/schema.rb
# Enable pending cops so we can adopt the code before they are switched on. # Enable pending cops so we can adopt the code before they are switched on.
NewCops: enable NewCops: enable
# See "Lowest supported ruby version" in CONTRIBUTING.md # See "Lowest supported ruby version" in CONTRIBUTING.md
TargetRubyVersion: 2.5 TargetRubyVersion: 2.6
Bundler/OrderedGems:
Exclude:
- gemfiles/* # generated by Appraisal
Layout/ArgumentAlignment: Layout/ArgumentAlignment:
EnforcedStyle: with_fixed_indentation 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: Layout/DotPosition:
EnforcedStyle: trailing Enabled: false
# Avoid blank lines inside methods. They are a sign that the method is too big. # Avoid blank lines inside methods. They are a sign that the method is too big.
Layout/EmptyLineAfterGuardClause: Layout/EmptyLineAfterGuardClause:
@ -57,20 +56,11 @@ Layout/MultilineOperationIndentation:
Layout/ParameterAlignment: Layout/ParameterAlignment:
EnforcedStyle: with_fixed_indentation EnforcedStyle: with_fixed_indentation
Layout/SpaceAroundMethodCallOperator:
Enabled: true
# Use exactly one space on each side of an operator. Do not align operators # 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. # because it makes the code harder to edit, and makes lines unnecessarily long.
Layout/SpaceAroundOperators: Layout/SpaceAroundOperators:
AllowForAlignment: false AllowForAlignment: false
Lint/RaiseException:
Enabled: true
Lint/StructNewOverride:
Enabled: true
# Migrations often contain long up/down methods, and extracting smaller methods # Migrations often contain long up/down methods, and extracting smaller methods
# from these is of questionable value. # from these is of questionable value.
Metrics/AbcSize: Metrics/AbcSize:
@ -100,12 +90,9 @@ Naming/FileName:
- Appraisals - Appraisals
# Heredocs are usually assigned to a variable or constant, which already has a # 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 # name, so naming the delimiter doesn't add much value unless doing so improves
# heredocs that are used as anonymous values (not a variable, constant, or # syntax highlighting. For example, all heredocs containing SQL should be named
# named parameter). # SQL, to support editor syntax highlighting.
#
# All heredocs containing SQL should be named SQL, to support editor syntax
# highlighting.
Naming/HeredocDelimiterNaming: Naming/HeredocDelimiterNaming:
Enabled: false Enabled: false
@ -136,11 +123,6 @@ Rails/SkipsModelValidations:
RSpec/DescribeClass: RSpec/DescribeClass:
Enabled: false 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, # 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 # "no example will ever be longer than this"? Hard to say. Sometimes they're
# quite long. # quite long.
@ -164,10 +146,6 @@ Style/BlockDelimiters:
Style/DoubleNegation: Style/DoubleNegation:
Enabled: false Enabled: false
# This cop is unimportant in this repo.
Style/ExponentialNotation:
Enabled: false
# Avoid annotated tokens except in desperately complicated format strings. # Avoid annotated tokens except in desperately complicated format strings.
# In 99% of format strings they actually make it less readable. # In 99% of format strings they actually make it less readable.
Style/FormatStringToken: Style/FormatStringToken:

View File

@ -6,21 +6,6 @@
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again. # 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 # Offense count: 56
# Cop supports --auto-correct. # Cop supports --auto-correct.
Rails/ApplicationRecord: Rails/ApplicationRecord:

View File

@ -24,3 +24,8 @@ appraise "rails-6.1" do
gem "rails", "~> 6.1.0" gem "rails", "~> 6.1.0"
gem "rails-controller-testing", "~> 1.0.5" gem "rails-controller-testing", "~> 1.0.5"
end end
appraise "rails-7.0" do
gem "rails", "~> 7.0.0"
gem "rails-controller-testing", "~> 1.0.5"
end

View File

@ -17,6 +17,29 @@ recommendations of [keepachangelog.com](http://keepachangelog.com/).
- None - 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) ## 12.1.0 (2021-08-30)
### Breaking Changes ### Breaking Changes

View File

@ -15,7 +15,7 @@ This is the _user guide_. See also, the
Choose version: Choose version:
[Unreleased](https://github.com/paper-trail-gem/paper_trail/blob/master/README.md), [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), [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), [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), [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 end
``` ```
If you only use custom version classes and don't have a `versions` table, you If you only use custom version classes and don't have a `versions` table, you must
must let ActiveRecord know that the `PaperTrail::Version` class is an let ActiveRecord know that your base version class (eg. `ApplicationVersion` below)
`abstract_class`. class is an `abstract_class`.
```ruby ```ruby
# app/models/paper_trail/version.rb # app/models/application_version.rb
module PaperTrail class ApplicationVersion < ActiveRecord::Base
class Version < ActiveRecord::Base include PaperTrail::VersionConcern
include PaperTrail::VersionConcern self.abstract_class = true
self.abstract_class = true end
end
class PostVersion < ApplicationVersion
self.table_name = :post_versions
self.sequence_name = :post_versions_id_seq
end end
``` ```

View File

@ -38,11 +38,8 @@ task :clean do
end end
end end
desc <<~EOS desc "Create the database."
Write a database.yml for the specified RDBMS, and create database. Does not task :create_db do
migrate. Migration happens later in spec_helper.
EOS
task prepare: %i[clean install_database_yml] do
puts format("creating %s database", ENV["DB"]) puts format("creating %s database", ENV["DB"])
case ENV["DB"] case ENV["DB"]
when "mysql" when "mysql"
@ -59,6 +56,12 @@ task prepare: %i[clean install_database_yml] do
end end
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" require "rspec/core/rake_task"
desc "Run tests on PaperTrail with RSpec" desc "Run tests on PaperTrail with RSpec"
task(:spec).clear task(:spec).clear

View File

@ -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: "../"

View File

@ -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 # 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. # version of ActiveRecord with support for fractional seconds in MySQL.
# (https://github.com/rails/rails/pull/14359) # (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 t.datetime :created_at
end end
add_index :versions, %i(item_type item_id) add_index :versions, %i(item_type item_id)

View File

@ -26,6 +26,8 @@ module PaperTrail
named created_at. named created_at.
EOS EOS
RAILS_GTE_7_0 = ::ActiveRecord.gem_version >= ::Gem::Version.new("7.0.0")
extend PaperTrail::Cleaner extend PaperTrail::Cleaner
class << self class << self

View File

@ -32,6 +32,12 @@ module PaperTrail
if defined_enums[attr] && val.is_a?(::String) if defined_enums[attr] && val.is_a?(::String)
# Because PT 4 used to save the string version of enums to `object_changes` # Because PT 4 used to save the string version of enums to `object_changes`
val 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 else
AttributeSerializerFactory.for(@klass, attr).deserialize(val) AttributeSerializerFactory.for(@klass, attr).deserialize(val)
end end

View File

@ -8,7 +8,7 @@ module PaperTrail
# #
# It is not safe to assume that a new version of rails will be compatible with # 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 # 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 # However, as of
# [#1213](https://github.com/paper-trail-gem/paper_trail/pull/1213) our # [#1213](https://github.com/paper-trail-gem/paper_trail/pull/1213) our
@ -18,7 +18,7 @@ module PaperTrail
# versions. # versions.
module Compatibility module Compatibility
ACTIVERECORD_GTE = ">= 5.2" # enforced in gemspec 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 E_INCOMPATIBLE_AR = <<-EOS
PaperTrail %s is not compatible with ActiveRecord %s. We allow PT PaperTrail %s is not compatible with ActiveRecord %s. We allow PT

View File

@ -116,6 +116,20 @@ module PaperTrail
@changes_in_latest_version ||= load_changes_in_latest_version @changes_in_latest_version ||= load_changes_in_latest_version
end 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 # An attributed is "ignored" if it is listed in the `:ignore` option
# and/or the `:skip` option. Returns true if an ignored attribute has # and/or the `:skip` option. Returns true if an ignored attribute has
# changed. # changed.
@ -182,20 +196,28 @@ module PaperTrail
if value.respond_to?(:call) if value.respond_to?(:call)
value.call(@record) value.call(@record)
elsif value.is_a?(Symbol) && @record.respond_to?(value, true) elsif value.is_a?(Symbol) && @record.respond_to?(value, true)
# If it is an attribute that is changing in an existing object, metadatum_from_model_method(event, value)
# 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
else else
value value
end end
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 # @api private
def notable_changes def notable_changes
changes_in_latest_version.delete_if { |k, _v| changes_in_latest_version.delete_if { |k, _v|
@ -207,16 +229,9 @@ module PaperTrail
def notably_changed def notably_changed
# Memoized to reduce memory usage # Memoized to reduce memory usage
@notably_changed ||= begin @notably_changed ||= begin
only = @record.paper_trail_options[:only].dup only = evaluate_only
# Remove Hash arguments and then evaluate whether the attributes (the cani = changed_and_not_ignored
# keys of the hash) should also get pushed into the collection. only.empty? ? cani : (cani & only)
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)
end end
end end

View File

@ -35,16 +35,21 @@ module PaperTrail
if record_object? if record_object?
data[:object] = recordable_object(@is_touch) data[:object] = recordable_object(@is_touch)
end end
if record_object_changes? merge_object_changes_into(data)
changes = @force_changes.nil? ? notable_changes : @force_changes
data[:object_changes] = prepare_object_changes(changes)
end
merge_item_subtype_into(data) merge_item_subtype_into(data)
merge_metadata_into(data) merge_metadata_into(data)
end end
private 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 # `touch` cannot record `object_changes` because rails' `touch` does not
# perform dirty-tracking. Specifically, methods from `Dirty`, like # perform dirty-tracking. Specifically, methods from `Dirty`, like
# `saved_changes`, return the same values before and after `touch`. # `saved_changes`, return the same values before and after `touch`.

View File

@ -40,8 +40,7 @@ module PaperTrail
@model_class.after_create { |r| @model_class.after_create { |r|
r.paper_trail.record_create if r.paper_trail.save_version? r.paper_trail.record_create if r.paper_trail.save_version?
} }
return if @model_class.paper_trail_options[:on].include?(:create) append_option_uniquely(:on, :create)
@model_class.paper_trail_options[:on] << :create
end end
# Adds a callback that records a version before or after a "destroy" event. # Adds a callback that records a version before or after a "destroy" event.
@ -49,7 +48,6 @@ module PaperTrail
# @api public # @api public
def on_destroy(recording_order = "before") def on_destroy(recording_order = "before")
assert_valid_recording_order_for_on_destroy(recording_order) assert_valid_recording_order_for_on_destroy(recording_order)
@model_class.send( @model_class.send(
"#{recording_order}_destroy", "#{recording_order}_destroy",
lambda do |r| lambda do |r|
@ -57,9 +55,7 @@ module PaperTrail
r.paper_trail.record_destroy(recording_order) r.paper_trail.record_destroy(recording_order)
end end
) )
append_option_uniquely(:on, :destroy)
return if @model_class.paper_trail_options[:on].include?(:destroy)
@model_class.paper_trail_options[:on] << :destroy
end end
# Adds a callback that records a version after an "update" event. # Adds a callback that records a version after an "update" event.
@ -81,8 +77,7 @@ module PaperTrail
@model_class.after_update { |r| @model_class.after_update { |r|
r.paper_trail.clear_version_instance r.paper_trail.clear_version_instance
} }
return if @model_class.paper_trail_options[:on].include?(:update) append_option_uniquely(:on, :update)
@model_class.paper_trail_options[:on] << :update
end end
# Adds a callback that records a version after a "touch" event. # Adds a callback that records a version after a "touch" event.
@ -96,11 +91,13 @@ module PaperTrail
# @api public # @api public
def on_touch def on_touch
@model_class.after_touch { |r| @model_class.after_touch { |r|
r.paper_trail.record_update( if r.paper_trail.save_version?
force: RAILS_LT_6_0, r.paper_trail.record_update(
in_after_callback: true, force: RAILS_LT_6_0,
is_touch: true in_after_callback: true,
) is_touch: true
)
end
} }
end end
@ -127,6 +124,13 @@ module PaperTrail
RAILS_LT_6_0 = ::ActiveRecord.gem_version < ::Gem::Version.new("6.0.0") RAILS_LT_6_0 = ::ActiveRecord.gem_version < ::Gem::Version.new("6.0.0")
private_constant :RAILS_LT_6_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`. # Raises an error if the provided class is an `abstract_class`.
# @api private # @api private
def assert_concrete_activerecord_class(class_name) def assert_concrete_activerecord_class(class_name)
@ -205,6 +209,14 @@ module PaperTrail
options options
end 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) def get_versions_scope(options)
options[:versions][:scope] || -> { order(model.timestamp_sort_order) } options[:versions][:scope] || -> { order(model.timestamp_sort_order) }
end end
@ -239,12 +251,8 @@ module PaperTrail
@model_class.paper_trail_options = options.dup @model_class.paper_trail_options = options.dup
%i[ignore skip only].each do |k| %i[ignore skip only].each do |k|
@model_class.paper_trail_options[k] = [@model_class.paper_trail_options[k]]. @model_class.paper_trail_options[k] = event_attribute_option(k)
flatten.
compact.
map { |attr| attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s }
end end
@model_class.paper_trail_options[:meta] ||= {} @model_class.paper_trail_options[:meta] ||= {}
end end
end end

View File

@ -9,7 +9,7 @@ module PaperTrail
extend self # makes all instance methods become module methods as well extend self # makes all instance methods become module methods as well
def load(string) def load(string)
::YAML.load string ::YAML.respond_to?(:unsafe_load) ? ::YAML.unsafe_load(string) : ::YAML.load(string)
end end
# @param object (Hash | HashWithIndifferentAccess) - Coming from # @param object (Hash | HashWithIndifferentAccess) - Coming from

View File

@ -16,7 +16,7 @@ module PaperTrail
extend ::ActiveSupport::Concern extend ::ActiveSupport::Concern
included do included do
belongs_to :item, polymorphic: true, optional: true belongs_to :item, polymorphic: true, optional: true, inverse_of: false
validates_presence_of :event validates_presence_of :event
after_create :enforce_version_limit! after_create :enforce_version_limit!
end end
@ -376,10 +376,11 @@ module PaperTrail
# #
# @api private # @api private
def version_limit def version_limit
if limit_option?(item.class) klass = item.class
item.class.paper_trail_options[:limit] if limit_option?(klass)
elsif base_class_limit_option?(item.class) klass.paper_trail_options[:limit]
item.class.base_class.paper_trail_options[:limit] elsif base_class_limit_option?(klass)
klass.base_class.paper_trail_options[:limit]
else else
PaperTrail.config.version_limit PaperTrail.config.version_limit
end end

View File

@ -8,7 +8,7 @@ module PaperTrail
# People are encouraged to use `PaperTrail.gem_version` instead. # People are encouraged to use `PaperTrail.gem_version` instead.
module VERSION module VERSION
MAJOR = 12 MAJOR = 12
MINOR = 1 MINOR = 2
TINY = 0 TINY = 0
# Set PRE to nil unless it's a pre-release (beta, rc, etc.) # Set PRE to nil unless it's a pre-release (beta, rc, etc.)

View File

@ -43,9 +43,7 @@ has been destroyed.
# about 3 years, per https://www.ruby-lang.org/en/downloads/branches/ # about 3 years, per https://www.ruby-lang.org/en/downloads/branches/
# #
# See "Lowest supported ruby version" in CONTRIBUTING.md # See "Lowest supported ruby version" in CONTRIBUTING.md
# s.required_ruby_version = ">= 2.6.0"
# Ruby 2.5 reaches EoL on 2021-03-31.
s.required_ruby_version = ">= 2.5.0"
# We no longer specify a maximum activerecord version. # We no longer specify a maximum activerecord version.
# See discussion in paper_trail/compatibility.rb # See discussion in paper_trail/compatibility.rb
@ -53,8 +51,8 @@ has been destroyed.
s.add_dependency "request_store", "~> 1.1" s.add_dependency "request_store", "~> 1.1"
s.add_development_dependency "appraisal", "~> 2.4.1" s.add_development_dependency "appraisal", "~> 2.4.1"
s.add_development_dependency "byebug", "~> 11.0" s.add_development_dependency "byebug", "~> 11.1"
s.add_development_dependency "ffaker", "~> 2.19.0" s.add_development_dependency "ffaker", "~> 2.20"
s.add_development_dependency "generator_spec", "~> 0.9.4" s.add_development_dependency "generator_spec", "~> 0.9.4"
s.add_development_dependency "memory_profiler", "~> 1.0.0" 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 "rake", "~> 13.0"
s.add_development_dependency "rspec-rails", "~> 5.0.2" 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-packaging", "~> 0.5.1"
s.add_development_dependency "rubocop-performance", "~> 1.11.5" 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-rake", "~> 0.6.0"
s.add_development_dependency "rubocop-rspec", "~> 2.4.0" s.add_development_dependency "rubocop-rspec", "~> 2.5.0"
s.add_development_dependency "simplecov", ">= 0.21", "< 0.22" s.add_development_dependency "simplecov", "~> 0.21.2"
# ## Database Adapters # ## Database Adapters
# #
@ -83,7 +81,7 @@ has been destroyed.
# Currently, all versions of rails we test against are consistent. In the past, # 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 # when we tested against rails 4.2, we had to specify database adapters in
# `Appraisals`. # `Appraisals`.
s.add_development_dependency "mysql2", "~> 0.5" s.add_development_dependency "mysql2", "~> 0.5.3"
s.add_development_dependency "pg", ">= 0.18", "< 2.0" s.add_development_dependency "pg", "~> 1.2"
s.add_development_dependency "sqlite3", "~> 1.4" s.add_development_dependency "sqlite3", "~> 1.4"
end end

View File

@ -2,4 +2,6 @@
class Car < Vehicle class Car < Vehicle
has_paper_trail has_paper_trail
attribute :color, type: ActiveModel::Type::String
attr_accessor :top_speed
end end

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
# See also `Vegetable` which uses `JsonbVersion`.
class Fruit < ActiveRecord::Base class Fruit < ActiveRecord::Base
if ENV["DB"] == "postgres" || JsonVersion.table_exists? if ENV["DB"] == "postgres"
has_paper_trail versions: { class_name: "JsonVersion" } has_paper_trail versions: { class_name: "JsonVersion" }
end end
end end

View File

@ -2,12 +2,8 @@
# Demonstrates the `if` and `unless` configuration options. # Demonstrates the `if` and `unless` configuration options.
class Translation < ActiveRecord::Base 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( has_paper_trail(
if: proc { |t| t.language_code == "US" }, if: proc { |t| t.language_code == "US" },
unless: proc { |t| t.type == "DRAFT" } unless: proc { |t| t.draft_status == "DRAFT" }
) )
end end

View File

@ -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

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class JsonbVersion < ActiveRecord::Base
include PaperTrail::VersionConcern
self.table_name = "jsonb_versions"
end

View File

@ -128,16 +128,19 @@ class SetUpTestTables < ::ActiveRecord::Migration::Current
add_index :no_object_versions, %i[item_type item_id] add_index :no_object_versions, %i[item_type item_id]
if ENV["DB"] == "postgres" if ENV["DB"] == "postgres"
create_table :json_versions, force: true do |t| %w[json jsonb].each do |j|
t.string :item_type, null: false table_name = j + "_versions"
t.integer :item_id, null: false create_table table_name, force: true do |t|
t.string :event, null: false t.string :item_type, null: false
t.string :whodunnit t.bigint :item_id, null: false
t.json :object t.string :event, null: false
t.json :object_changes t.string :whodunnit
t.datetime :created_at, limit: 6 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 end
add_index :json_versions, %i[item_type item_id]
end end
create_table :not_on_updates, force: true do |t| create_table :not_on_updates, force: true do |t|
@ -249,10 +252,10 @@ class SetUpTestTables < ::ActiveRecord::Migration::Current
end end
create_table :translations, force: true do |t| create_table :translations, force: true do |t|
t.string :headline
t.string :content t.string :content
t.string :draft_status
t.string :headline
t.string :language_code t.string :language_code
t.string :type
end end
create_table :gadgets, force: true do |t| create_table :gadgets, force: true do |t|
@ -277,8 +280,9 @@ class SetUpTestTables < ::ActiveRecord::Migration::Current
end end
create_table :fruits, force: true do |t| create_table :fruits, force: true do |t|
t.string :name
t.string :color t.string :color
t.integer :mass
t.string :name
end end
create_table :boolits, force: true do |t| create_table :boolits, force: true do |t|
@ -358,6 +362,12 @@ class SetUpTestTables < ::ActiveRecord::Migration::Current
t.integer :parent_id t.integer :parent_id
t.integer :partner_id t.integer :partner_id
end end
create_table :vegetables, force: true do |t|
t.string :color
t.integer :mass
t.string :name
end
end end
def down def down

View File

@ -4,8 +4,8 @@ require "spec_helper"
RSpec.describe Animal, type: :model, versioning: true do RSpec.describe Animal, type: :model, versioning: true do
it "baseline test setup" do it "baseline test setup" do
expect(Animal.new).to be_versioned expect(described_class.new).to be_versioned
expect(Animal.inheritance_column).to eq("species") expect(described_class.inheritance_column).to eq("species")
end end
describe "#descends_from_active_record?" do describe "#descends_from_active_record?" do
@ -15,7 +15,7 @@ RSpec.describe Animal, type: :model, versioning: true do
end end
it "works with custom STI inheritance column" do 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 from the Muppets")
animal.update(name: "Animal Muppet") animal.update(name: "Animal Muppet")
animal.destroy animal.destroy
@ -46,7 +46,7 @@ RSpec.describe Animal, type: :model, versioning: true do
it "allows the inheritance_column (species) to be updated" do it "allows the inheritance_column (species) to be updated" do
cat = Cat.create!(name: "Leo") cat = Cat.create!(name: "Leo")
cat.update(name: "Spike", species: "Dog") cat.update(name: "Spike", species: "Dog")
dog = Animal.find(cat.id) dog = described_class.find(cat.id)
expect(dog).to be_instance_of(Dog) expect(dog).to be_instance_of(Dog)
end end

View File

@ -187,7 +187,7 @@ RSpec.describe Article, type: :model, versioning: true do
end end
context "with an item" do context "with an item" do
let(:article) { Article.new(title: initial_title) } let(:article) { described_class.new(title: initial_title) }
let(:initial_title) { "Foobar" } let(:initial_title) { "Foobar" }
context "when it is created" do context "when it is created" do

View File

@ -5,7 +5,7 @@ require "spec_helper"
RSpec.describe Book, versioning: true do RSpec.describe Book, versioning: true do
context "with :has_many :through" do context "with :has_many :through" do
it "store version on source <<" 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") dostoyevsky = Person.create(name: "Dostoyevsky")
Person.create(name: "Solzhenitsyn") Person.create(name: "Solzhenitsyn")
count = PaperTrail::Version.count count = PaperTrail::Version.count
@ -15,7 +15,7 @@ RSpec.describe Book, versioning: true do
end end
it "store version on source create" do 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: "Dostoyevsky")
Person.create(name: "Solzhenitsyn") Person.create(name: "Solzhenitsyn")
count = PaperTrail::Version.count count = PaperTrail::Version.count
@ -27,7 +27,7 @@ RSpec.describe Book, versioning: true do
end end
it "store version on join destroy" do 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") dostoyevsky = Person.create(name: "Dostoyevsky")
Person.create(name: "Solzhenitsyn") Person.create(name: "Solzhenitsyn")
(book.authors << dostoyevsky) (book.authors << dostoyevsky)
@ -39,7 +39,7 @@ RSpec.describe Book, versioning: true do
end end
it "store version on join clear" do 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") dostoyevsky = Person.create(name: "Dostoyevsky")
Person.create(name: "Solzhenitsyn") Person.create(name: "Solzhenitsyn")
book.authors << dostoyevsky book.authors << dostoyevsky
@ -53,7 +53,7 @@ RSpec.describe Book, versioning: true do
context "when a persisted record is updated then destroyed" do context "when a persisted record is updated then destroyed" do
it "has changes" 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"] changes = YAML.load book.versions.last.attributes["object_changes"]
expect(changes).to eq("id" => [nil, book.id], "title" => [nil, "A"]) expect(changes).to eq("id" => [nil, book.id], "title" => [nil, "A"])

View File

@ -4,7 +4,7 @@ require "spec_helper"
require "support/custom_json_serializer" require "support/custom_json_serializer"
RSpec.describe Boolit, type: :model, versioning: true do RSpec.describe Boolit, type: :model, versioning: true do
let(:boolit) { Boolit.create! } let(:boolit) { described_class.create! }
before { boolit.update!(name: FFaker::Name.name) } before { boolit.update!(name: FFaker::Name.name) }
@ -20,7 +20,7 @@ RSpec.describe Boolit, type: :model, versioning: true do
before { boolit.update!(scoped: false) } before { boolit.update!(scoped: false) }
it "is NOT scoped" do it "is NOT scoped" do
expect(Boolit.first).to be_nil expect(described_class.first).to be_nil
end end
it "still can be reified and persisted" do it "still can be reified and persisted" do

View File

@ -7,9 +7,28 @@ RSpec.describe Car, type: :model do
describe "changeset", versioning: true do describe "changeset", versioning: true do
it "has the expected keys (see issue 738)" 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") car.update(name: "Bob")
assert_includes car.versions.last.changeset.keys, "name" assert_includes car.versions.last.changeset.keys, "name"
end end
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 end

View File

@ -12,9 +12,9 @@ RSpec.describe CustomPrimaryKeyRecord, type: :model do
version = custom_primary_key_record.versions.last version = custom_primary_key_record.versions.last
expect(version).to be_a(CustomPrimaryKeyRecordVersion) expect(version).to be_a(CustomPrimaryKeyRecordVersion)
version_from_db = CustomPrimaryKeyRecordVersion.last 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 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 end
end end

View File

@ -5,7 +5,7 @@ require "spec_helper"
RSpec.describe Document, type: :model, versioning: true do RSpec.describe Document, type: :model, versioning: true do
describe "have_a_version_with matcher" do describe "have_a_version_with matcher" do
it "works with custom versions association" do it "works with custom versions association" do
document = Document.create!(name: "Foo") document = described_class.create!(name: "Foo")
document.update!(name: "Bar") document.update!(name: "Bar")
expect(document).to have_a_version_with(name: "Foo") expect(document).to have_a_version_with(name: "Foo")
end end
@ -13,7 +13,7 @@ RSpec.describe Document, type: :model, versioning: true do
describe "#paper_trail.next_version" do describe "#paper_trail.next_version" do
it "returns the expected document" do it "returns the expected document" do
doc = Document.create doc = described_class.create
doc.update(name: "Doc 1") doc.update(name: "Doc 1")
reified = doc.paper_trail_versions.last.reify reified = doc.paper_trail_versions.last.reify
expect(doc.name).to(eq(reified.paper_trail.next_version.name)) 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 describe "#paper_trail.previous_version" do
it "returns the expected document" do it "returns the expected document" do
doc = Document.create doc = described_class.create
doc.update(name: "Doc 1") doc.update(name: "Doc 1")
doc.update(name: "Doc 2") doc.update(name: "Doc 2")
expect(doc.paper_trail_versions.length).to(eq(3)) 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 describe "#paper_trail_versions" do
it "returns the expected version records" do it "returns the expected version records" do
doc = Document.create doc = described_class.create
doc.update(name: "Doc 1") doc.update(name: "Doc 1")
expect(doc.paper_trail_versions.length).to(eq(2)) expect(doc.paper_trail_versions.length).to(eq(2))
expect(doc.paper_trail_versions.map(&:event)).to( expect(doc.paper_trail_versions.map(&:event)).to(
@ -43,7 +43,7 @@ RSpec.describe Document, type: :model, versioning: true do
describe "#versions" do describe "#versions" do
it "does not respond to versions method" do it "does not respond to versions method" do
doc = Document.create doc = described_class.create
doc.update(name: "Doc 1") doc.update(name: "Doc 1")
expect(doc).not_to respond_to(:versions) expect(doc).not_to respond_to(:versions)
end end

View File

@ -5,7 +5,7 @@ require "support/performance_helpers"
RSpec.describe(FooWidget, versioning: true) do RSpec.describe(FooWidget, versioning: true) do
context "with a subclass" do context "with a subclass" do
let(:foo) { FooWidget.create } let(:foo) { described_class.create }
before do before do
foo.update!(name: "Foo") foo.update!(name: "Foo")
@ -26,7 +26,7 @@ RSpec.describe(FooWidget, versioning: true) do
before { foo.destroy } before { foo.destroy }
it "reify with the correct type" do 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.previous).to(eq(foo.versions[1]))
expect(PaperTrail::Version.last.next).to(be_nil) expect(PaperTrail::Version.last.next).to(be_nil)
end end

View File

@ -2,19 +2,54 @@
require "spec_helper" 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 RSpec.describe Fruit, type: :model, versioning: true do
describe "have_a_version_with_changes matcher" do describe "have_a_version_with_changes matcher" do
it "works with Fruit because Fruit uses JsonVersion" do it "works with Fruit because Fruit uses JsonVersion" do
# As of PT 9.0.0, with_version_changes only supports json(b) columns, # 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 # so that's why were testing the have_a_version_with_changes matcher
# here. # here.
banana = Fruit.create!(color: "Red", name: "Banana") banana = described_class.create!(color: "Red", name: "Banana")
banana.update!(color: "Yellow") banana.update!(color: "Yellow")
expect(banana).to have_a_version_with_changes(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: "Pink")
expect(banana).not_to have_a_version_with_changes(color: "Yellow", name: "Kiwi") expect(banana).not_to have_a_version_with_changes(color: "Yellow", name: "Kiwi")
end end
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
end end

View File

@ -3,7 +3,7 @@
require "spec_helper" require "spec_helper"
RSpec.describe Gadget, type: :model do 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 } it { is_expected.to be_versioned }
@ -35,7 +35,11 @@ RSpec.describe Gadget, type: :model do
gadget.update_attribute(:updated_at, Time.current + 1) gadget.update_attribute(:updated_at, Time.current + 1)
}.to(change { gadget.versions.size }.by(1)) }.to(change { gadget.versions.size }.by(1))
expect( 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"]) ).to eq(["updated_at"])
end end
end end

View File

@ -4,10 +4,10 @@ require "spec_helper"
RSpec.describe JoinedVersion, type: :model, versioning: true do RSpec.describe JoinedVersion, type: :model, versioning: true do
let(:widget) { Widget.create!(name: FFaker::Name.name) } let(:widget) { Widget.create!(name: FFaker::Name.name) }
let(:version) { JoinedVersion.first } let(:version) { described_class.first }
describe "default_scope" do describe "default_scope" do
it { expect(JoinedVersion.default_scopes).not_to be_empty } it { expect(described_class.default_scopes).not_to be_empty }
end end
describe "VersionConcern::ClassMethods" do describe "VersionConcern::ClassMethods" do
@ -15,19 +15,19 @@ RSpec.describe JoinedVersion, type: :model, versioning: true do
describe "#subsequent" do describe "#subsequent" do
it "does not raise error when there is a default_scope that joins" 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
end end
describe "#preceding" do describe "#preceding" do
it "does not raise error when there is a default scope that joins" 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
end end
describe "#between" do describe "#between" do
it "does not raise error when there is a default scope that joins" 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 end
end end

View File

@ -5,7 +5,7 @@ require "spec_helper"
# The `json_versions` table tests postgres' `json` data type. So, that # The `json_versions` table tests postgres' `json` data type. So, that
# table is only created when testing against postgres. # table is only created when testing against postgres.
if JsonVersion.table_exists? if JsonVersion.table_exists?
RSpec.describe JsonVersion, type: :model do RSpec.describe JsonVersion, type: :model, versioning: true do
it "includes the VersionConcern module" do it "includes the VersionConcern module" do
expect(described_class).to include(PaperTrail::VersionConcern) expect(described_class).to include(PaperTrail::VersionConcern)
end end

View File

@ -27,7 +27,11 @@ RSpec.describe NoObject, versioning: true do
# New feature: destroy populates object_changes # New feature: destroy populates object_changes
# https://github.com/paper-trail-gem/paper_trail/pull/1123 # 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["id"]).to eq([n.id, nil])
expect(h["letter"]).to eq([n.letter, nil]) expect(h["letter"]).to eq([n.letter, nil])
expect(h["created_at"][0]).to be_present expect(h["created_at"][0]).to be_present
@ -38,7 +42,7 @@ RSpec.describe NoObject, versioning: true do
describe "reify" do describe "reify" do
it "raises error" do it "raises error" do
n = NoObject.create!(letter: "A") n = described_class.create!(letter: "A")
v = n.versions.last v = n.versions.last
expect { v.reify }.to( expect { v.reify }.to(
raise_error( raise_error(
@ -51,7 +55,7 @@ RSpec.describe NoObject, versioning: true do
describe "where_object" do describe "where_object" do
it "raises error" do it "raises error" do
n = NoObject.create!(letter: "A") n = described_class.create!(letter: "A")
expect { expect {
n.versions.where_object(foo: "bar") n.versions.where_object(foo: "bar")
}.to( }.to(

View File

@ -9,21 +9,21 @@ require "spec_helper"
RSpec.describe Person, type: :model, versioning: true do RSpec.describe Person, type: :model, versioning: true do
describe "#time_zone" do describe "#time_zone" do
it "returns an ActiveSupport::TimeZone" 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)) expect(person.time_zone.class).to(eq(ActiveSupport::TimeZone))
end end
end end
context "when the model is saved" do context "when the model is saved" do
it "version.object_changes should store long serialization of TimeZone object" 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! person.save!
len = person.versions.last.object_changes.length len = person.versions.last.object_changes.length
expect((len < 105)).to(be_truthy) expect((len < 105)).to(be_truthy)
end end
it "version.object_changes attribute should have stored the value from serializer" do 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! person.save!
as_stored_in_version = HashWithIndifferentAccess[ as_stored_in_version = HashWithIndifferentAccess[
YAML.load(person.versions.last.object_changes) YAML.load(person.versions.last.object_changes)
@ -34,14 +34,14 @@ RSpec.describe Person, type: :model, versioning: true do
end end
it "version.changeset should convert attribute to original, unserialized value" do 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.save!
unserialized_value = Person::TimeZoneSerializer.load(person.time_zone) unserialized_value = Person::TimeZoneSerializer.load(person.time_zone)
expect(person.versions.last.changeset[:time_zone].last).to(eq(unserialized_value)) expect(person.versions.last.changeset[:time_zone].last).to(eq(unserialized_value))
end end
it "record.changes (before save) returns the original, unserialized values" do 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 changes_before_save = person.changes.dup
person.save! person.save!
expect( expect(
@ -50,7 +50,7 @@ RSpec.describe Person, type: :model, versioning: true do
end end
it "version.changeset should be the same as record.changes was before the save" do 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 changes_before_save = person.changes.dup
person.save! person.save!
actual = person.versions.last.changeset.delete_if { |k, _v| (k.to_sym == :id) } 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 context "when that attribute is updated" do
it "object should not store long serialization of TimeZone object" 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.save!
person.assign_attributes(time_zone: "Pacific Time (US & Canada)") person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
person.save! person.save!
@ -70,7 +70,7 @@ RSpec.describe Person, type: :model, versioning: true do
end end
it "object_changes should not store long serialization of TimeZone object" do 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.save!
person.assign_attributes(time_zone: "Pacific Time (US & Canada)") person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
person.save! person.save!
@ -79,7 +79,7 @@ RSpec.describe Person, type: :model, versioning: true do
end end
it "version.object attribute should have stored value from serializer" do 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! person.save!
attribute_value_before_change = person.time_zone attribute_value_before_change = person.time_zone
person.assign_attributes(time_zone: "Pacific Time (US & Canada)") person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
@ -93,7 +93,7 @@ RSpec.describe Person, type: :model, versioning: true do
end end
it "version.object_changes attribute should have stored value from serializer" do 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.save!
person.assign_attributes(time_zone: "Pacific Time (US & Canada)") person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
person.save! person.save!
@ -106,7 +106,7 @@ RSpec.describe Person, type: :model, versioning: true do
end end
it "version.reify should convert attribute to original, unserialized value" do 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! person.save!
attribute_value_before_change = person.time_zone attribute_value_before_change = person.time_zone
person.assign_attributes(time_zone: "Pacific Time (US & Canada)") person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
@ -116,7 +116,7 @@ RSpec.describe Person, type: :model, versioning: true do
end end
it "version.changeset should convert attribute to original, unserialized value" do 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.save!
person.assign_attributes(time_zone: "Pacific Time (US & Canada)") person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
person.save! person.save!
@ -125,7 +125,7 @@ RSpec.describe Person, type: :model, versioning: true do
end end
it "record.changes (before save) returns the original, unserialized values" do 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.save!
person.assign_attributes(time_zone: "Pacific Time (US & Canada)") person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
changes_before_save = person.changes.dup changes_before_save = person.changes.dup
@ -136,7 +136,7 @@ RSpec.describe Person, type: :model, versioning: true do
end end
it "version.changeset should be the same as record.changes was before the save" do 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.save!
person.assign_attributes(time_zone: "Pacific Time (US & Canada)") person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
changes_before_save = person.changes.dup changes_before_save = person.changes.dup
@ -151,7 +151,7 @@ RSpec.describe Person, type: :model, versioning: true do
describe "#cars and bicycles" do describe "#cars and bicycles" do
it "can be reified" do it "can be reified" do
person = Person.create(name: "Frank") person = described_class.create(name: "Frank")
car = Car.create(name: "BMW 325") car = Car.create(name: "BMW 325")
bicycle = Bicycle.create(name: "BMX 1.0") bicycle = Bicycle.create(name: "BMX 1.0")

View File

@ -5,7 +5,7 @@ require "rails/generators"
RSpec.describe Pet, type: :model, versioning: true do RSpec.describe Pet, type: :model, versioning: true do
it "baseline test setup" do it "baseline test setup" do
expect(Pet.new).to be_versioned expect(described_class.new).to be_versioned
end end
it "can be reified" do it "can be reified" do
@ -13,8 +13,8 @@ RSpec.describe Pet, type: :model, versioning: true do
dog = Dog.create(name: "Snoopy") dog = Dog.create(name: "Snoopy")
cat = Cat.create(name: "Garfield") cat = Cat.create(name: "Garfield")
person.pets << Pet.create(animal: dog) person.pets << described_class.create(animal: dog)
person.pets << Pet.create(animal: cat) person.pets << described_class.create(animal: cat)
person.update(name: "Steve") person.update(name: "Steve")
dog.update(name: "Beethoven") dog.update(name: "Beethoven")

View File

@ -4,8 +4,8 @@ require "spec_helper"
RSpec.describe Plant, type: :model, versioning: true do RSpec.describe Plant, type: :model, versioning: true do
it "baseline test setup" do it "baseline test setup" do
expect(Plant.new).to be_versioned expect(described_class.new).to be_versioned
expect(Plant.inheritance_column).to eq("species") expect(described_class.inheritance_column).to eq("species")
end end
describe "#descends_from_active_record?" do describe "#descends_from_active_record?" do
@ -15,14 +15,14 @@ RSpec.describe Plant, type: :model, versioning: true do
end end
it "works with non standard STI column contents" do it "works with non standard STI column contents" do
plant = Plant.create plant = described_class.create
plant.destroy plant.destroy
tomato = Tomato.create tomato = Tomato.create
tomato.destroy tomato.destroy
reified = plant.versions.last.reify reified = plant.versions.last.reify
expect(reified.class).to eq(Plant) expect(reified.class).to eq(described_class)
reified = tomato.versions.last.reify reified = tomato.versions.last.reify
expect(reified.class).to eq(Tomato) expect(reified.class).to eq(Tomato)

View File

@ -5,7 +5,7 @@ require "spec_helper"
# The `Post` model uses a custom version class, `PostVersion` # The `Post` model uses a custom version class, `PostVersion`
RSpec.describe Post, type: :model, versioning: true do RSpec.describe Post, type: :model, versioning: true do
it "inserts records into the correct table, post_versions" do it "inserts records into the correct table, post_versions" do
post = Post.create post = described_class.create
expect(PostVersion.count).to(eq(1)) expect(PostVersion.count).to(eq(1))
post.update(content: "Some new content") post.update(content: "Some new content")
expect(PostVersion.count).to(eq(2)) expect(PostVersion.count).to(eq(2))
@ -14,20 +14,20 @@ RSpec.describe Post, type: :model, versioning: true do
context "with the first version" do context "with the first version" do
it "have the correct index" do it "have the correct index" do
post = Post.create post = described_class.create
version = post.versions.first version = post.versions.first
expect(version.index).to(eq(0)) expect(version.index).to(eq(0))
end end
end end
it "have versions of the custom class" do it "have versions of the custom class" do
post = Post.create post = described_class.create
expect(post.versions.first.class.name).to(eq("PostVersion")) expect(post.versions.first.class.name).to(eq("PostVersion"))
end end
describe "#changeset" do describe "#changeset" do
it "returns nil because the object_changes column doesn't exist" 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") post.update(content: "Some new content")
expect(post.versions.last.changeset).to(be_nil) expect(post.versions.last.changeset).to(be_nil)
end end

View File

@ -11,7 +11,7 @@ RSpec.describe Skipper, type: :model, versioning: true do
let(:t2) { Time.zone.local(2015, 7, 15, 20, 34, 30) } let(:t2) { Time.zone.local(2015, 7, 15, 20, 34, 30) }
it "does not create a version" do it "does not create a version" do
skipper = Skipper.create!(another_timestamp: t1) skipper = described_class.create!(another_timestamp: t1)
expect { expect {
skipper.update!(another_timestamp: t2) skipper.update!(another_timestamp: t2)
}.not_to(change { skipper.versions.length }) }.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") if ActiveRecord.gem_version >= Gem::Version.new("6")
it "does not create a version for skipped attributes" do it "does not create a version for skipped attributes" do
skipper = Skipper.create!(another_timestamp: t1) skipper = described_class.create!(another_timestamp: t1)
expect { expect {
skipper.touch(:another_timestamp, time: t2) skipper.touch(:another_timestamp, time: t2)
}.not_to(change { skipper.versions.length }) }.not_to(change { skipper.versions.length })
end end
it "does not create a version for ignored attributes" do it "does not create a version for ignored attributes" do
skipper = Skipper.create!(created_at: t1) skipper = described_class.create!(created_at: t1)
expect { expect {
skipper.touch(:created_at, time: t2) skipper.touch(:created_at, time: t2)
}.not_to(change { skipper.versions.length }) }.not_to(change { skipper.versions.length })
end end
else else
it "creates a version even for skipped attributes" do it "creates a version even for skipped attributes" do
skipper = Skipper.create!(another_timestamp: t1) skipper = described_class.create!(another_timestamp: t1)
expect { expect {
skipper.touch(:another_timestamp, time: t2) skipper.touch(:another_timestamp, time: t2)
}.to(change { skipper.versions.length }) }.to(change { skipper.versions.length })
end end
it "creates a version even for ignored attributes" do it "creates a version even for ignored attributes" do
skipper = Skipper.create!(created_at: t1) skipper = described_class.create!(created_at: t1)
expect { expect {
skipper.touch(:created_at, time: t2) skipper.touch(:created_at, time: t2)
}.to(change { skipper.versions.length }) }.to(change { skipper.versions.length })
@ -54,7 +54,7 @@ RSpec.describe Skipper, type: :model, versioning: true do
end end
it "creates a version for non-skipped timestamps" do it "creates a version for non-skipped timestamps" do
skipper = Skipper.create! skipper = described_class.create!
expect { expect {
skipper.touch skipper.touch
}.to(change { skipper.versions.length }) }.to(change { skipper.versions.length })
@ -67,7 +67,7 @@ RSpec.describe Skipper, type: :model, versioning: true do
context "without preserve (default)" do context "without preserve (default)" do
it "has no timestamp" 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.update!(another_timestamp: t2, name: "Foobar")
skipper = skipper.versions.last.reify skipper = skipper.versions.last.reify
expect(skipper.another_timestamp).to be(nil) expect(skipper.another_timestamp).to be(nil)
@ -76,7 +76,7 @@ RSpec.describe Skipper, type: :model, versioning: true do
context "with preserve" do context "with preserve" do
it "preserves its timestamp" 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.update!(another_timestamp: t2, name: "Foobar")
skipper = skipper.versions.last.reify(unversioned_attributes: :preserve) skipper = skipper.versions.last.reify(unversioned_attributes: :preserve)
expect(skipper.another_timestamp).to eq(t2) expect(skipper.another_timestamp).to eq(t2)

View File

@ -4,10 +4,10 @@ require "spec_helper"
RSpec.describe Thing, type: :model do RSpec.describe Thing, type: :model do
describe "#versions", versioning: true do describe "#versions", versioning: true do
let(:thing) { Thing.create! } let(:thing) { described_class.create! }
it "applies the scope option" do 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" expect(thing.versions.to_sql).to end_with "ORDER BY id desc"
end end

View File

@ -24,6 +24,14 @@ RSpec.describe Translation, type: :model, versioning: true do
expect(PaperTrail::Version.count).to(eq(0)) expect(PaperTrail::Version.count).to(eq(0))
end end
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 end
context "with US translations" do 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 it "creation does not change the number of versions" do
translation = described_class.new(headline: "Headline") translation = described_class.new(headline: "Headline")
translation.language_code = "US" translation.language_code = "US"
translation.type = "DRAFT" translation.draft_status = "DRAFT"
translation.save! translation.save!
expect(PaperTrail::Version.count).to(eq(0)) expect(PaperTrail::Version.count).to(eq(0))
end end
@ -39,11 +47,20 @@ RSpec.describe Translation, type: :model, versioning: true do
it "update does not change the number of versions" do it "update does not change the number of versions" do
translation = described_class.new(headline: "Headline") translation = described_class.new(headline: "Headline")
translation.language_code = "US" translation.language_code = "US"
translation.type = "DRAFT" translation.draft_status = "DRAFT"
translation.save! translation.save!
translation.update(content: "Content") translation.update(content: "Content")
expect(PaperTrail::Version.count).to(eq(0)) expect(PaperTrail::Version.count).to(eq(0))
end 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 end
context "with non-drafts" do context "with non-drafts" do
@ -52,14 +69,21 @@ RSpec.describe Translation, type: :model, versioning: true do
expect(PaperTrail::Version.count).to(eq(1)) expect(PaperTrail::Version.count).to(eq(1))
end 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 = described_class.create!(headline: "Headline", language_code: "US")
translation.update(content: "Content") translation.update(content: "Content")
expect(PaperTrail::Version.count).to(eq(2)) expect(PaperTrail::Version.count).to(eq(2))
expect(translation.versions.size).to(eq(2)) expect(translation.versions.size).to(eq(2))
end 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 = described_class.new(headline: "Headline")
translation.language_code = "US" translation.language_code = "US"
translation.save! translation.save!

View File

@ -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

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require "spec_helper" require "spec_helper"
require "support/shared_examples/queries"
module PaperTrail module PaperTrail
::RSpec.describe Version, type: :model do ::RSpec.describe Version, type: :model do
@ -60,7 +61,7 @@ module PaperTrail
describe "#paper_trail_originator" do describe "#paper_trail_originator" do
context "with no previous versions" do context "with no previous versions" do
it "returns nil" 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
end end
@ -78,7 +79,7 @@ module PaperTrail
describe "#previous" do describe "#previous" do
context "with no previous versions" do context "with no previous versions" do
it "returns nil" do it "returns nil" do
expect(PaperTrail::Version.new.previous).to be_nil expect(described_class.new.previous).to be_nil
end end
end end
@ -88,7 +89,7 @@ module PaperTrail
widget = Widget.create!(name: FFaker::Name.name) widget = Widget.create!(name: FFaker::Name.name)
widget.versions.first.update!(whodunnit: name) widget.versions.first.update!(whodunnit: name)
widget.update!(name: FFaker::Name.first_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 end
end end
@ -96,442 +97,39 @@ module PaperTrail
describe "#terminator" do describe "#terminator" do
it "is an alias for the `whodunnit` attribute" do it "is an alias for the `whodunnit` attribute" do
attributes = { whodunnit: FFaker::Name.first_name } attributes = { whodunnit: FFaker::Name.first_name }
version = PaperTrail::Version.new(attributes) version = described_class.new(attributes)
expect(version.terminator).to eq(attributes[:whodunnit]) expect(version.terminator).to eq(attributes[:whodunnit])
end end
end end
describe "#version_author" do describe "#version_author" do
it "is an alias for the `terminator` method" 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)) expect(version.method(:version_author)).to eq(version.method(:terminator))
end end
end end
context "when changing the data type of database columns on the fly" do context "with text columns", versioning: true do
# TODO: Changing the data type of these database columns in the middle include_examples "queries", :text, ::Widget, :an_integer
# of the test suite adds a fair amount of complexity. Is there a better end
# way? We already have a `json_versions` table in our tests, maybe we
# could use that and add a `jsonb_versions` table? if ENV["DB"] == "postgres"
column_overrides = [false] context "with json columns", versioning: true do
if ENV["DB"] == "postgres" include_examples(
column_overrides += %w[json jsonb] "queries",
:json,
::Fruit, # uses JsonVersion
:mass
)
end end
column_overrides.shuffle.each do |column_datatype_override| context "with jsonb columns", versioning: true do
context "with a #{column_datatype_override || 'text'} column" do include_examples(
let(:widget) { Widget.new } "queries",
let(:name) { FFaker::Name.first_name } :jsonb,
let(:int) { column_datatype_override ? 1 : rand(2..6) } ::Vegetable, # uses JsonbVersion
:mass
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
end end
end end
end end

View File

@ -6,7 +6,7 @@ require "support/performance_helpers"
RSpec.describe Widget, type: :model, versioning: true do RSpec.describe Widget, type: :model, versioning: true do
describe "#changeset" do describe "#changeset" do
it "has expected values" do it "has expected values" do
widget = Widget.create(name: "Henry") widget = described_class.create(name: "Henry")
changeset = widget.versions.last.changeset changeset = widget.versions.last.changeset
expect(changeset["name"]).to eq([nil, "Henry"]) expect(changeset["name"]).to eq([nil, "Henry"])
expect(changeset["id"]).to eq([nil, widget.id]) expect(changeset["id"]).to eq([nil, widget.id])
@ -24,7 +24,7 @@ RSpec.describe Widget, type: :model, versioning: true do
end end
it "calls the adapter's load_changeset method" do it "calls the adapter's load_changeset method" do
widget = Widget.create(name: "Henry") widget = described_class.create(name: "Henry")
adapter = instance_spy("CustomObjectChangesAdapter") adapter = instance_spy("CustomObjectChangesAdapter")
PaperTrail.config.object_changes_adapter = adapter PaperTrail.config.object_changes_adapter = adapter
allow(adapter).to( allow(adapter).to(
@ -39,7 +39,7 @@ RSpec.describe Widget, type: :model, versioning: true do
it "defaults to the original behavior" do it "defaults to the original behavior" do
adapter = Class.new.new adapter = Class.new.new
PaperTrail.config.object_changes_adapter = adapter PaperTrail.config.object_changes_adapter = adapter
widget = Widget.create(name: "Henry") widget = described_class.create(name: "Henry")
changeset = widget.versions.last.changeset changeset = widget.versions.last.changeset
expect(changeset[:name]).to eq([nil, "Henry"]) expect(changeset[:name]).to eq([nil, "Henry"])
end end
@ -48,44 +48,44 @@ RSpec.describe Widget, type: :model, versioning: true do
context "with a new record" do context "with a new record" do
it "not have any previous versions" do it "not have any previous versions" do
expect(Widget.new.versions).to(eq([])) expect(described_class.new.versions).to(eq([]))
end end
it "be live" do 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
end end
context "with a persisted record" do context "with a persisted record" do
it "have one previous version" 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)) expect(widget.versions.length).to(eq(1))
end end
it "be nil in its previous version" do 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.object).to(be_nil)
expect(widget.versions.first.reify).to(be_nil) expect(widget.versions.first.reify).to(be_nil)
end end
it "record the correct event" do 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)) expect(widget.versions.first.event).to(match(/create/i))
end end
it "be live" do it "be live" do
widget = Widget.create(name: "Henry") widget = described_class.create(name: "Henry")
expect(widget.paper_trail.live?).to(eq(true)) expect(widget.paper_trail.live?).to(eq(true))
end end
it "use the widget `updated_at` as the version's `created_at`" do 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)) expect(widget.versions.first.created_at.to_i).to(eq(widget.updated_at.to_i))
end end
context "when updated without any changes" do context "when updated without any changes" do
it "to have two previous versions" do it "to have two previous versions" do
widget = Widget.create(name: "Henry") widget = described_class.create(name: "Henry")
widget.touch widget.touch
expect(widget.versions.length).to eq(2) expect(widget.versions.length).to eq(2)
end end
@ -93,13 +93,13 @@ RSpec.describe Widget, type: :model, versioning: true do
context "when updated with changes" do context "when updated with changes" do
it "have three previous versions" do it "have three previous versions" do
widget = Widget.create(name: "Henry") widget = described_class.create(name: "Henry")
widget.update(name: "Harry") widget.update(name: "Harry")
expect(widget.versions.length).to(eq(2)) expect(widget.versions.length).to(eq(2))
end end
it "be available in its previous version" do it "be available in its previous version" do
widget = Widget.create(name: "Henry") widget = described_class.create(name: "Henry")
widget.update(name: "Harry") widget.update(name: "Harry")
expect(widget.name).to(eq("Harry")) expect(widget.name).to(eq("Harry"))
expect(widget.versions.last.object).not_to(be_nil) expect(widget.versions.last.object).not_to(be_nil)
@ -109,19 +109,19 @@ RSpec.describe Widget, type: :model, versioning: true do
end end
it "have the same ID in its previous version" do 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") widget.update(name: "Harry")
expect(widget.versions.last.reify.id).to(eq(widget.id)) expect(widget.versions.last.reify.id).to(eq(widget.id))
end end
it "record the correct event" do it "record the correct event" do
widget = Widget.create(name: "Henry") widget = described_class.create(name: "Henry")
widget.update(name: "Harry") widget.update(name: "Harry")
expect(widget.versions.last.event).to(match(/update/i)) expect(widget.versions.last.event).to(match(/update/i))
end end
it "have versions that are not live" do it "have versions that are not live" do
widget = Widget.create(name: "Henry") widget = described_class.create(name: "Henry")
widget.update(name: "Harry") widget.update(name: "Harry")
widget.versions.map(&:reify).compact.each do |v| widget.versions.map(&:reify).compact.each do |v|
expect(v.paper_trail).not_to be_live expect(v.paper_trail).not_to be_live
@ -129,7 +129,7 @@ RSpec.describe Widget, type: :model, versioning: true do
end end
it "have stored changes" do it "have stored changes" do
widget = Widget.create(name: "Henry") widget = described_class.create(name: "Henry")
widget.update(name: "Harry") widget.update(name: "Harry")
last_obj_changes = widget.versions.last.object_changes last_obj_changes = widget.versions.last.object_changes
actual = PaperTrail.serializer.load(last_obj_changes).reject do |k, _v| actual = PaperTrail.serializer.load(last_obj_changes).reject do |k, _v|
@ -141,7 +141,7 @@ RSpec.describe Widget, type: :model, versioning: true do
end end
it "return changes with indifferent access" do it "return changes with indifferent access" do
widget = Widget.create(name: "Henry") widget = described_class.create(name: "Henry")
widget.update(name: "Harry") 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]))
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 context "when updated, and has one associated object" do
it "not copy the has_one association by default when reifying" 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") widget.update(name: "Harry")
wotsit = widget.create_wotsit name: "John" wotsit = widget.create_wotsit name: "John"
reified_widget = widget.versions.last.reify 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 context "when updated, and has many associated objects" do
it "copy the has_many associations when reifying" 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.update(name: "Harry")
widget.fluxors.create(name: "f-zero") widget.fluxors.create(name: "f-zero")
widget.fluxors.create(name: "f-one") 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 context "when updated, and has many associated polymorphic objects" do
it "copy the has_many associations when reifying" 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.update(name: "Harry")
widget.whatchamajiggers.create(name: "f-zero") widget.whatchamajiggers.create(name: "f-zero")
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 context "when updated, polymorphic objects by themselves" do
it "not fail with a nil pointer on the polymorphic association" 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.update(name: "Harry")
widget = Whatchamajigger.new(name: "f-zero") widget = Whatchamajigger.new(name: "f-zero")
widget.save! widget.save!
@ -198,21 +198,21 @@ RSpec.describe Widget, type: :model, versioning: true do
context "when updated, and then destroyed" do context "when updated, and then destroyed" do
it "record the correct event" do it "record the correct event" do
widget = Widget.create(name: "Henry") widget = described_class.create(name: "Henry")
widget.update(name: "Harry") widget.update(name: "Harry")
widget.destroy widget.destroy
expect(PaperTrail::Version.last.event).to(match(/destroy/i)) expect(PaperTrail::Version.last.event).to(match(/destroy/i))
end end
it "have three previous versions" do it "have three previous versions" do
widget = Widget.create(name: "Henry") widget = described_class.create(name: "Henry")
widget.update(name: "Harry") widget.update(name: "Harry")
widget.destroy widget.destroy
expect(PaperTrail::Version.with_item_keys("Widget", widget.id).length).to(eq(3)) expect(PaperTrail::Version.with_item_keys("Widget", widget.id).length).to(eq(3))
end end
it "returns the expected attributes for the reified widget" do 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.update(name: "Harry")
widget.destroy widget.destroy
reified_widget = PaperTrail::Version.last.reify reified_widget = PaperTrail::Version.last.reify
@ -239,7 +239,7 @@ RSpec.describe Widget, type: :model, versioning: true do
end end
it "be re-creatable from its previous version" do 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.update(name: "Harry")
widget.destroy widget.destroy
reified_widget = PaperTrail::Version.last.reify reified_widget = PaperTrail::Version.last.reify
@ -247,7 +247,7 @@ RSpec.describe Widget, type: :model, versioning: true do
end end
it "restore its associations on its previous version" do 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.update(name: "Harry")
widget.fluxors.create(name: "flux") widget.fluxors.create(name: "flux")
widget.destroy widget.destroy
@ -257,9 +257,11 @@ RSpec.describe Widget, type: :model, versioning: true do
end end
it "have nil item for last version" do it "have nil item for last version" do
widget = Widget.create(name: "Henry") widget = described_class.create(name: "Henry")
widget.update(name: "Harry") widget.update(name: "Harry")
widget.destroy 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 expect(widget.versions.last.item).to be_nil
end end
end end
@ -270,7 +272,7 @@ RSpec.describe Widget, type: :model, versioning: true do
let!(:t0) { Time.current } let!(:t0) { Time.current }
let(:previous_widget) { widget.versions.last.reify } let(:previous_widget) { widget.versions.last.reify }
let(:widget) { let(:widget) {
Widget.create( described_class.create(
name: "Warble", name: "Warble",
a_text: "The quick brown fox", a_text: "The quick brown fox",
an_integer: 42, an_integer: 42,
@ -338,7 +340,7 @@ RSpec.describe Widget, type: :model, versioning: true do
let(:last_version) { widget.versions.last } let(:last_version) { widget.versions.last }
it "reify previous version" do it "reify previous version" do
assert_kind_of(Widget, last_version.reify) assert_kind_of(described_class, last_version.reify)
end end
it "restore all forward-compatible attributes" do it "restore all forward-compatible attributes" do
@ -362,7 +364,7 @@ RSpec.describe Widget, type: :model, versioning: true do
after { PaperTrail.enabled = true } after { PaperTrail.enabled = true }
it "not add to its trail" do it "not add to its trail" do
widget = Widget.create(name: "Zaphod") widget = described_class.create(name: "Zaphod")
PaperTrail.enabled = false PaperTrail.enabled = false
count = widget.versions.length count = widget.versions.length
widget.update(name: "Beeblebrox") 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 context "with its paper trail turned off, when updated" do
after do after do
PaperTrail.request.enable_model(Widget) PaperTrail.request.enable_model(described_class)
end end
it "not add to its trail" do it "not add to its trail" do
widget = Widget.create(name: "Zaphod") widget = described_class.create(name: "Zaphod")
PaperTrail.request.disable_model(Widget) PaperTrail.request.disable_model(described_class)
count = widget.versions.length count = widget.versions.length
widget.update(name: "Beeblebrox") widget.update(name: "Beeblebrox")
expect(widget.versions.length).to(eq(count)) expect(widget.versions.length).to(eq(count))
end end
it "add to its trail" do it "add to its trail" do
widget = Widget.create(name: "Zaphod") widget = described_class.create(name: "Zaphod")
PaperTrail.request.disable_model(Widget) PaperTrail.request.disable_model(described_class)
count = widget.versions.length count = widget.versions.length
widget.update(name: "Beeblebrox") widget.update(name: "Beeblebrox")
PaperTrail.request.enable_model(Widget) PaperTrail.request.enable_model(described_class)
widget.update(name: "Ford") widget.update(name: "Ford")
expect(widget.versions.length).to(eq((count + 1))) expect(widget.versions.length).to(eq((count + 1)))
end end
@ -398,7 +400,7 @@ RSpec.describe Widget, type: :model, versioning: true do
context "with somebody making changes" do context "with somebody making changes" do
context "when a record is created" do context "when a record is created" do
it "tracks who made the change" do it "tracks who made the change" do
widget = Widget.new(name: "Fidget") widget = described_class.new(name: "Fidget")
PaperTrail.request.whodunnit = "Alice" PaperTrail.request.whodunnit = "Alice"
widget.save widget.save
version = widget.versions.last version = widget.versions.last
@ -411,7 +413,7 @@ RSpec.describe Widget, type: :model, versioning: true do
context "when created, then updated" do context "when created, then updated" do
it "tracks who made the change" do it "tracks who made the change" do
widget = Widget.new(name: "Fidget") widget = described_class.new(name: "Fidget")
PaperTrail.request.whodunnit = "Alice" PaperTrail.request.whodunnit = "Alice"
widget.save widget.save
PaperTrail.request.whodunnit = "Bob" PaperTrail.request.whodunnit = "Bob"
@ -426,7 +428,7 @@ RSpec.describe Widget, type: :model, versioning: true do
context "when created, updated, and destroyed" do context "when created, updated, and destroyed" do
it "tracks who made the change" do it "tracks who made the change" do
widget = Widget.new(name: "Fidget") widget = described_class.new(name: "Fidget")
PaperTrail.request.whodunnit = "Alice" PaperTrail.request.whodunnit = "Alice"
widget.save widget.save
PaperTrail.request.whodunnit = "Bob" PaperTrail.request.whodunnit = "Bob"
@ -444,7 +446,7 @@ RSpec.describe Widget, type: :model, versioning: true do
context "with an item with versions" do context "with an item with versions" do
context "when the versions were created over time" 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(:t0) { 2.days.ago }
let(:t1) { 1.day.ago } let(:t1) { 1.day.ago }
let(:t2) { 1.hour.ago } let(:t2) { 1.hour.ago }
@ -501,7 +503,7 @@ RSpec.describe Widget, type: :model, versioning: true do
describe ".versions_between" do describe ".versions_between" do
it "return versions in the time period" 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: "Fidget")
widget.update(name: "Digit") widget.update(name: "Digit")
widget.versions[0].update(created_at: 30.days.ago) widget.versions[0].update(created_at: 30.days.ago)
@ -524,11 +526,11 @@ RSpec.describe Widget, type: :model, versioning: true do
end end
context "with the first version" do context "with the first version" do
let(:widget) { Widget.create(name: "Widget") } let(:widget) { described_class.create(name: "Widget") }
let(:version) { widget.versions.last } let(:version) { widget.versions.last }
before do before do
widget = Widget.create(name: "Widget") widget = described_class.create(name: "Widget")
widget.update(name: "Fidget") widget.update(name: "Fidget")
widget.update(name: "Digit") widget.update(name: "Digit")
end end
@ -547,7 +549,7 @@ RSpec.describe Widget, type: :model, versioning: true do
end end
context "with the last version" do context "with the last version" do
let(:widget) { Widget.create(name: "Widget") } let(:widget) { described_class.create(name: "Widget") }
let(:version) { widget.versions.last } let(:version) { widget.versions.last }
before do before do
@ -571,7 +573,7 @@ RSpec.describe Widget, type: :model, versioning: true do
context "with a reified item" do context "with a reified item" do
it "know which version it came from, and return its previous self" 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| %w[Tom Dick Jane].each do |name|
widget.update(name: name) widget.update(name: name)
end end
@ -585,7 +587,7 @@ RSpec.describe Widget, type: :model, versioning: true do
describe "#next_version" do describe "#next_version" do
context "with a reified item" do context "with a reified item" do
it "returns the object (not a Version) as it became next" 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| %w[Tom Dick Jane].each do |name|
widget.update(name: name) widget.update(name: name)
end end
@ -598,7 +600,7 @@ RSpec.describe Widget, type: :model, versioning: true do
context "with a non-reified item" do context "with a non-reified item" do
it "always returns nil because cannot ever have a next version" 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) expect(widget.paper_trail.next_version).to(be_nil)
widget.save widget.save
%w[Tom Dick Jane].each do |name| %w[Tom Dick Jane].each do |name|
@ -612,7 +614,7 @@ RSpec.describe Widget, type: :model, versioning: true do
describe "#previous_version" do describe "#previous_version" do
context "with a reified item" do context "with a reified item" do
it "returns the object (not a Version) as it was most recently" 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| %w[Tom Dick Jane].each do |name|
widget.update(name: name) widget.update(name: name)
end end
@ -625,7 +627,7 @@ RSpec.describe Widget, type: :model, versioning: true do
context "with a non-reified item" do context "with a non-reified item" do
it "returns the object (not a Version) as it was most recently" 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) expect(widget.paper_trail.previous_version).to(be_nil)
widget.save widget.save
%w[Tom Dick Jane].each do |name| %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 context "with an unsaved record" do
it "not have a version created on destroy" do it "not have a version created on destroy" do
widget = Widget.new widget = described_class.new
widget.destroy widget.destroy
expect(widget.versions.empty?).to(eq(true)) expect(widget.versions.empty?).to(eq(true))
end end
@ -646,7 +648,7 @@ RSpec.describe Widget, type: :model, versioning: true do
context "when measuring the memory allocation of" do context "when measuring the memory allocation of" do
let(:widget) do let(:widget) do
Widget.new( described_class.new(
name: "Warble", name: "Warble",
a_text: "The quick brown fox", a_text: "The quick brown fox",
an_integer: 42, an_integer: 42,
@ -725,7 +727,7 @@ RSpec.describe Widget, type: :model, versioning: true do
end end
describe "`have_a_version_with` matcher", versioning: true do 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 before do
widget.update!(name: "Leonard", an_integer: 1) widget.update!(name: "Leonard", an_integer: 1)
@ -743,21 +745,21 @@ RSpec.describe Widget, type: :model, versioning: true do
describe "versioning option" do describe "versioning option" do
context "when enabled", versioning: true do context "when enabled", versioning: true do
it "enables versioning" 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) expect(widget.versions.size).to eq(1)
end end
end end
context "when disabled", versioning: false do context "when disabled", versioning: false do
it "does not enable versioning" 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) expect(widget.versions.size).to eq(0)
end end
end end
end end
describe "Callbacks", versioning: true do 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 describe "before_save" do
it "resets value for timestamp attrs for update so that value gets updated properly" 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 end
describe "after_create" do 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 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) 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 end
describe "Association", versioning: true do 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 describe "sort order" do
it "sorts by the timestamp order from the `VersionConcern`" do it "sorts by the timestamp order from the `VersionConcern`" do
@ -844,19 +846,19 @@ RSpec.describe Widget, type: :model, versioning: true do
end end
describe "#create", versioning: true do 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 it "creates a version record" do
wordget = Widget.create wordget = described_class.create
assert_equal 1, wordget.versions.length assert_equal 1, wordget.versions.length
end end
end end
describe "#destroy", versioning: true do 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 it "creates a version record" do
widget = Widget.create widget = described_class.create
assert_equal 1, widget.versions.length assert_equal 1, widget.versions.length
widget.destroy widget.destroy
versions_for_widget = PaperTrail::Version.with_item_keys("Widget", widget.id) 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`. # the `widget.versions` association, instead of `with_item_keys`.
PaperTrail::Version.with_item_keys("Widget", widget.id) PaperTrail::Version.with_item_keys("Widget", widget.id)
} }
widget = Widget.create widget = described_class.create
assert_equal 1, widget.versions.length assert_equal 1, widget.versions.length
widget.destroy widget.destroy
assert_equal 2, versions.call(widget).length assert_equal 2, versions.call(widget).length
@ -883,7 +885,7 @@ RSpec.describe Widget, type: :model, versioning: true do
end end
describe "#paper_trail.originator", versioning: true do 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 describe "return value" do
let(:orig_name) { FFaker::Name.name } let(:orig_name) { FFaker::Name.name }
@ -923,7 +925,7 @@ RSpec.describe Widget, type: :model, versioning: true do
end end
describe "#version_at", versioning: true do 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 context "when Timestamp argument is AFTER object has been destroyed" do
it "returns nil" do it "returns nil" do
@ -935,7 +937,7 @@ RSpec.describe Widget, type: :model, versioning: true do
end end
describe "touch", versioning: true do 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 it "creates a version" do
expect { widget.touch }.to change { expect { widget.touch }.to change {
@ -953,10 +955,10 @@ RSpec.describe Widget, type: :model, versioning: true do
end end
describe ".paper_trail.update_columns", versioning: true do 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 it "creates a version record" do
widget = Widget.create widget = described_class.create
expect(widget.versions.count).to eq(1) expect(widget.versions.count).to eq(1)
widget.paper_trail.update_columns(name: "Bugle") widget.paper_trail.update_columns(name: "Bugle")
expect(widget.versions.count).to eq(2) expect(widget.versions.count).to eq(2)
@ -966,10 +968,10 @@ RSpec.describe Widget, type: :model, versioning: true do
end end
describe "#update", versioning: true do 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 it "creates a version record" do
widget = Widget.create widget = described_class.create
assert_equal 1, widget.versions.length assert_equal 1, widget.versions.length
widget.update(name: "Bugle") widget.update(name: "Bugle")
assert_equal 2, widget.versions.length assert_equal 2, widget.versions.length

View File

@ -4,7 +4,7 @@ require "spec_helper"
RSpec.describe Wotsit, versioning: true do RSpec.describe Wotsit, versioning: true do
it "update! records timestamps" do it "update! records timestamps" do
wotsit = Wotsit.create!(name: "wotsit") wotsit = described_class.create!(name: "wotsit")
wotsit.update!(name: "changed") wotsit.update!(name: "changed")
reified = wotsit.versions.last.reify reified = wotsit.versions.last.reify
expect(reified.created_at).not_to(be_nil) expect(reified.created_at).not_to(be_nil)
@ -12,7 +12,7 @@ RSpec.describe Wotsit, versioning: true do
end end
it "update! does not raise error" do 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) expect { wotsit.update!(name: "name2") }.not_to(raise_error)
end end
end end

View File

@ -14,7 +14,7 @@ module PaperTrail
context "when incompatible" do context "when incompatible" do
it "writes a warning to stderr" 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 { expect {
described_class.check_activerecord(ar_version) described_class.check_activerecord(ar_version)
}.to output(/not compatible/).to_stderr }.to output(/not compatible/).to_stderr

View File

@ -9,7 +9,7 @@ module PaperTrail
context "with a new record" do context "with a new record" do
it "returns true" do it "returns true" do
g = Gadget.new(created_at: Time.current) 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) expect(event.changed_notably?).to eq(true)
end end
end end
@ -18,14 +18,14 @@ module PaperTrail
it "only acknowledges non-ignored attrs" do it "only acknowledges non-ignored attrs" do
gadget = Gadget.create!(created_at: Time.current) gadget = Gadget.create!(created_at: Time.current)
gadget.name = "Wrench" gadget.name = "Wrench"
event = PaperTrail::Events::Base.new(gadget, false) event = described_class.new(gadget, false)
expect(event.changed_notably?).to eq(true) expect(event.changed_notably?).to eq(true)
end end
it "does not acknowledge ignored attr (brand)" do it "does not acknowledge ignored attr (brand)" do
gadget = Gadget.create!(created_at: Time.current) gadget = Gadget.create!(created_at: Time.current)
gadget.brand = "Acme" gadget.brand = "Acme"
event = PaperTrail::Events::Base.new(gadget, false) event = described_class.new(gadget, false)
expect(event.changed_notably?).to eq(false) expect(event.changed_notably?).to eq(false)
end end
end end
@ -35,7 +35,7 @@ module PaperTrail
gadget = Gadget.create!(created_at: Time.current) gadget = Gadget.create!(created_at: Time.current)
gadget.name = "Wrench" gadget.name = "Wrench"
gadget.updated_at = Time.current 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) expect(event.changed_notably?).to eq(true)
end end
@ -43,7 +43,7 @@ module PaperTrail
gadget = Gadget.create!(created_at: Time.current) gadget = Gadget.create!(created_at: Time.current)
gadget.brand = "Acme" gadget.brand = "Acme"
gadget.updated_at = Time.current 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) expect(event.changed_notably?).to eq(false)
end end
end end
@ -53,7 +53,7 @@ module PaperTrail
it "returns a hash lacking the skipped attribute" do it "returns a hash lacking the skipped attribute" do
# Skipper has_paper_trail(..., skip: [:another_timestamp]) # Skipper has_paper_trail(..., skip: [:another_timestamp])
skipper = Skipper.create!(another_timestamp: Time.current) 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) attributes = event.send(:nonskipped_attributes_before_change, false)
expect(attributes).not_to have_key("another_timestamp") expect(attributes).not_to have_key("another_timestamp")
end end

View File

@ -11,17 +11,21 @@ module PaperTrail
name: "Carter", name: "Carter",
path_to_stardom: "Mexican radio" 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_type]).to eq("Family::Family")
expect(data[:item_subtype]).to eq("Family::CelebrityFamily") expect(data[:item_subtype]).to eq("Family::CelebrityFamily")
end end
context "with skipper" do context "with skipper" do
let(:skipper) { Skipper.create!(another_timestamp: Time.current) } 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 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["id"]).to eq(skipper.id)
expect(object).to have_key("updated_at") expect(object).to have_key("updated_at")
expect(object).to have_key("created_at") expect(object).to have_key("created_at")
@ -29,7 +33,11 @@ module PaperTrail
end end
it "includes `object_changes` without skipped and ignored attributes" do 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["id"]).to eq([skipper.id, nil])
expect(changes["updated_at"][0]).to be_present expect(changes["updated_at"][0]).to be_present
expect(changes["updated_at"][1]).to be_nil expect(changes["updated_at"][1]).to be_nil

View File

@ -13,7 +13,7 @@ module PaperTrail
path_to_stardom: "Mexican radio" path_to_stardom: "Mexican radio"
) )
carter.path_to_stardom = "Johnny" 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( expect(data[:object_changes]).to eq(
<<~YAML <<~YAML
--- ---
@ -32,7 +32,7 @@ module PaperTrail
path_to_stardom: "Mexican radio" path_to_stardom: "Mexican radio"
) )
carter.path_to_stardom = "Johnny" 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 expect(data[:object_changes]).to be_nil
end end
end end

View File

@ -22,6 +22,19 @@ module PaperTrail
expect(described_class.load(hash.to_yaml)).to eq(hash) expect(described_class.load(hash.to_yaml)).to eq(hash)
expect(described_class.load(array.to_yaml)).to eq(array) expect(described_class.load(array.to_yaml)).to eq(array)
end 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 end
describe ".dump" do describe ".dump" do

View File

@ -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

View File

@ -7,7 +7,7 @@ require "simplecov"
SimpleCov.start do SimpleCov.start do
add_filter "spec" add_filter "spec"
end end
SimpleCov.minimum_coverage 92.4 SimpleCov.minimum_coverage(ENV["DB"] == "postgres" ? 97.3 : 92.4)
require "byebug" require "byebug"
require_relative "support/pt_arel_helpers" require_relative "support/pt_arel_helpers"
@ -53,7 +53,7 @@ end
# in `dummy_app/config/*`. By consolidating it here, # in `dummy_app/config/*`. By consolidating it here,
# #
# - It can better be understood, and documented in one place # - 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. # gems (like rspec-rails) _before_ loading the app.
# First, `config/boot.rb` would add gems to $LOAD_PATH. # First, `config/boot.rb` would add gems to $LOAD_PATH.

View File

@ -18,7 +18,7 @@ class PaperTrailSpecMigrator
@migrations_path = dummy_app_migrations_dir @migrations_path = dummy_app_migrations_dir
end 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, # in rails 5.2. This is an undocumented change, AFAICT. Then again,
# how many people use the programmatic interface? Most people probably # how many people use the programmatic interface? Most people probably
# just use rake. Maybe we're doing it wrong. # just use rake. Maybe we're doing it wrong.

View File

@ -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