mirror of
https://github.com/paper-trail-gem/paper_trail.git
synced 2022-11-09 11:33:19 -05:00
Move tests from model_spec to various specific model specs
Organizing by model has its disadvantages, but it's at least an easy scheme to understand. Extract spec/models/foo_widget_spec.rb Extract spec/models/wotsit_spec.rb Move tests to article_spec.rb Extract spec/models/book_spec.rb Move tests to song_spec.rb Move test to book_spec.rb Move tests to widget_spec.rb
This commit is contained in:
parent
8f5a935c3a
commit
a79b25aaca
8 changed files with 960 additions and 925 deletions
|
@ -94,6 +94,7 @@ Rails/WhereNot:
|
|||
Security/YAMLLoad:
|
||||
Exclude:
|
||||
- 'lib/paper_trail/serializers/yaml.rb'
|
||||
- 'spec/models/book_spec.rb'
|
||||
- 'spec/models/gadget_spec.rb'
|
||||
- 'spec/models/no_object_spec.rb'
|
||||
- 'spec/models/person_spec.rb'
|
||||
|
|
|
@ -185,4 +185,79 @@ RSpec.describe Article, type: :model, versioning: true do
|
|||
expect(article.versions.map(&:event)).to(match_array(%w[create destroy]))
|
||||
end
|
||||
end
|
||||
|
||||
context "with an item" do
|
||||
let(:article) { Article.new(title: initial_title) }
|
||||
let(:initial_title) { "Foobar" }
|
||||
|
||||
context "when it is created" do
|
||||
before { article.save }
|
||||
|
||||
it "store fixed meta data" do
|
||||
expect(article.versions.last.answer).to(eq(42))
|
||||
end
|
||||
|
||||
it "store dynamic meta data which is independent of the item" do
|
||||
expect(article.versions.last.question).to(eq("31 + 11 = 42"))
|
||||
end
|
||||
|
||||
it "store dynamic meta data which depends on the item" do
|
||||
expect(article.versions.last.article_id).to(eq(article.id))
|
||||
end
|
||||
|
||||
it "store dynamic meta data based on a method of the item" do
|
||||
expect(article.versions.last.action).to(eq(article.action_data_provider_method))
|
||||
end
|
||||
|
||||
it "store dynamic meta data based on an attribute of the item at creation" do
|
||||
expect(article.versions.last.title).to(eq(initial_title))
|
||||
end
|
||||
end
|
||||
|
||||
context "when it is created, then updated" do
|
||||
before do
|
||||
article.save
|
||||
article.update!(content: "Better text.", title: "Rhubarb")
|
||||
end
|
||||
|
||||
it "store fixed meta data" do
|
||||
expect(article.versions.last.answer).to(eq(42))
|
||||
end
|
||||
|
||||
it "store dynamic meta data which is independent of the item" do
|
||||
expect(article.versions.last.question).to(eq("31 + 11 = 42"))
|
||||
end
|
||||
|
||||
it "store dynamic meta data which depends on the item" do
|
||||
expect(article.versions.last.article_id).to(eq(article.id))
|
||||
end
|
||||
|
||||
it "store dynamic meta data based on an attribute of the item prior to the update" do
|
||||
expect(article.versions.last.title).to(eq(initial_title))
|
||||
end
|
||||
end
|
||||
|
||||
context "when it is created, then destroyed" do
|
||||
before do
|
||||
article.save
|
||||
article.destroy
|
||||
end
|
||||
|
||||
it "store fixed metadata" do
|
||||
expect(article.versions.last.answer).to(eq(42))
|
||||
end
|
||||
|
||||
it "store dynamic metadata which is independent of the item" do
|
||||
expect(article.versions.last.question).to(eq("31 + 11 = 42"))
|
||||
end
|
||||
|
||||
it "store dynamic metadata which depends on the item" do
|
||||
expect(article.versions.last.article_id).to(eq(article.id))
|
||||
end
|
||||
|
||||
it "store dynamic metadata based on attribute of item prior to destruction" do
|
||||
expect(article.versions.last.title).to(eq(initial_title))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
69
spec/models/book_spec.rb
Normal file
69
spec/models/book_spec.rb
Normal file
|
@ -0,0 +1,69 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe Book, versioning: true do
|
||||
context "with :has_many :through" do
|
||||
it "store version on source <<" do
|
||||
book = Book.create(title: "War and Peace")
|
||||
dostoyevsky = Person.create(name: "Dostoyevsky")
|
||||
Person.create(name: "Solzhenitsyn")
|
||||
count = PaperTrail::Version.count
|
||||
(book.authors << dostoyevsky)
|
||||
expect((PaperTrail::Version.count - count)).to(eq(1))
|
||||
expect(book.authorships.first.versions.first).to(eq(PaperTrail::Version.last))
|
||||
end
|
||||
|
||||
it "store version on source create" do
|
||||
book = Book.create(title: "War and Peace")
|
||||
Person.create(name: "Dostoyevsky")
|
||||
Person.create(name: "Solzhenitsyn")
|
||||
count = PaperTrail::Version.count
|
||||
book.authors.create(name: "Tolstoy")
|
||||
expect((PaperTrail::Version.count - count)).to(eq(2))
|
||||
expect(
|
||||
[PaperTrail::Version.order(:id).to_a[-2].item, PaperTrail::Version.last.item]
|
||||
).to match_array([Person.last, Authorship.last])
|
||||
end
|
||||
|
||||
it "store version on join destroy" do
|
||||
book = Book.create(title: "War and Peace")
|
||||
dostoyevsky = Person.create(name: "Dostoyevsky")
|
||||
Person.create(name: "Solzhenitsyn")
|
||||
(book.authors << dostoyevsky)
|
||||
count = PaperTrail::Version.count
|
||||
book.authorships.reload.last.destroy
|
||||
expect((PaperTrail::Version.count - count)).to(eq(1))
|
||||
expect(PaperTrail::Version.last.reify.book).to(eq(book))
|
||||
expect(PaperTrail::Version.last.reify.author).to(eq(dostoyevsky))
|
||||
end
|
||||
|
||||
it "store version on join clear" do
|
||||
book = Book.create(title: "War and Peace")
|
||||
dostoyevsky = Person.create(name: "Dostoyevsky")
|
||||
Person.create(name: "Solzhenitsyn")
|
||||
book.authors << dostoyevsky
|
||||
count = PaperTrail::Version.count
|
||||
book.authorships.reload.destroy_all
|
||||
expect((PaperTrail::Version.count - count)).to(eq(1))
|
||||
expect(PaperTrail::Version.last.reify.book).to(eq(book))
|
||||
expect(PaperTrail::Version.last.reify.author).to(eq(dostoyevsky))
|
||||
end
|
||||
end
|
||||
|
||||
context "when a persisted record is updated then destroyed" do
|
||||
it "has changes" do
|
||||
book = Book.create! title: "A"
|
||||
changes = YAML.load book.versions.last.attributes["object_changes"]
|
||||
expect(changes).to eq("id" => [nil, book.id], "title" => [nil, "A"])
|
||||
|
||||
book.update! title: "B"
|
||||
changes = YAML.load book.versions.last.attributes["object_changes"]
|
||||
expect(changes).to eq("title" => %w[A B])
|
||||
|
||||
book.destroy
|
||||
changes = YAML.load book.versions.last.attributes["object_changes"]
|
||||
expect(changes).to eq("id" => [book.id, nil], "title" => ["B", nil])
|
||||
end
|
||||
end
|
||||
end
|
35
spec/models/foo_widget_spec.rb
Normal file
35
spec/models/foo_widget_spec.rb
Normal file
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
require "support/performance_helpers"
|
||||
|
||||
RSpec.describe(FooWidget, versioning: true) do
|
||||
context "with a subclass" do
|
||||
let(:foo) { FooWidget.create }
|
||||
|
||||
before do
|
||||
foo.update!(name: "Foo")
|
||||
end
|
||||
|
||||
it "reify with the correct type" do
|
||||
expect(PaperTrail::Version.last.previous).to(eq(foo.versions.first))
|
||||
expect(PaperTrail::Version.last.next).to(be_nil)
|
||||
end
|
||||
|
||||
it "returns the correct originator" do
|
||||
PaperTrail.request.whodunnit = "Ben"
|
||||
foo.update_attribute(:name, "Geoffrey")
|
||||
expect(foo.paper_trail.originator).to(eq(PaperTrail.request.whodunnit))
|
||||
end
|
||||
|
||||
context "when destroyed" do
|
||||
before { foo.destroy }
|
||||
|
||||
it "reify with the correct type" do
|
||||
assert_kind_of(FooWidget, foo.versions.last.reify)
|
||||
expect(PaperTrail::Version.last.previous).to(eq(foo.versions[1]))
|
||||
expect(PaperTrail::Version.last.next).to(be_nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -14,4 +14,25 @@ require "spec_helper"
|
|||
expect(result.event).to eq("create")
|
||||
end
|
||||
end
|
||||
|
||||
context "when the default accessor, length=, is overwritten" do
|
||||
it "returns overwritten value on reified instance" do
|
||||
song = Song.create(length: 4)
|
||||
song.update(length: 5)
|
||||
expect(song.length).to(eq(5))
|
||||
expect(song.versions.last.reify.length).to(eq(4))
|
||||
end
|
||||
end
|
||||
|
||||
context "when song name is a virtual attribute (no such db column)" do
|
||||
it "returns overwritten virtual attribute on the reified instance" do
|
||||
song = Song.create(length: 4)
|
||||
song.update(length: 5)
|
||||
song.name = "Good Vibrations"
|
||||
song.save
|
||||
song.name = "Yellow Submarine"
|
||||
expect(song.name).to(eq("Yellow Submarine"))
|
||||
expect(song.versions.last.reify.name).to(eq("Good Vibrations"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,15 +1,732 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
require "support/performance_helpers"
|
||||
|
||||
RSpec.describe Widget, type: :model do
|
||||
let(:widget) { Widget.create! name: "Bob", an_integer: 1 }
|
||||
RSpec.describe Widget, type: :model, versioning: true do
|
||||
describe "#changeset" do
|
||||
it "has expected values" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
changeset = widget.versions.last.changeset
|
||||
expect(changeset["name"]).to eq([nil, "Henry"])
|
||||
expect(changeset["id"]).to eq([nil, widget.id])
|
||||
# When comparing timestamps, round off to the nearest second, because
|
||||
# mysql doesn't do fractional seconds.
|
||||
expect(changeset["created_at"][0]).to be_nil
|
||||
expect(changeset["created_at"][1].to_i).to eq(widget.created_at.to_i)
|
||||
expect(changeset["updated_at"][0]).to be_nil
|
||||
expect(changeset["updated_at"][1].to_i).to eq(widget.updated_at.to_i)
|
||||
end
|
||||
|
||||
context "with custom object_changes_adapter" do
|
||||
after do
|
||||
PaperTrail.config.object_changes_adapter = nil
|
||||
end
|
||||
|
||||
it "calls the adapter's load_changeset method" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
adapter = instance_spy("CustomObjectChangesAdapter")
|
||||
PaperTrail.config.object_changes_adapter = adapter
|
||||
allow(adapter).to(
|
||||
receive(:load_changeset).with(widget.versions.last).and_return(a: "b", c: "d")
|
||||
)
|
||||
changeset = widget.versions.last.changeset
|
||||
expect(changeset[:a]).to eq("b")
|
||||
expect(changeset[:c]).to eq("d")
|
||||
expect(adapter).to have_received(:load_changeset)
|
||||
end
|
||||
|
||||
it "defaults to the original behavior" do
|
||||
adapter = Class.new.new
|
||||
PaperTrail.config.object_changes_adapter = adapter
|
||||
widget = Widget.create(name: "Henry")
|
||||
changeset = widget.versions.last.changeset
|
||||
expect(changeset[:name]).to eq([nil, "Henry"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with a new record" do
|
||||
it "not have any previous versions" do
|
||||
expect(Widget.new.versions).to(eq([]))
|
||||
end
|
||||
|
||||
it "be live" do
|
||||
expect(Widget.new.paper_trail.live?).to(eq(true))
|
||||
end
|
||||
end
|
||||
|
||||
context "with a persisted record" do
|
||||
it "have one previous version" do
|
||||
widget = Widget.create(name: "Henry", created_at: (Time.current - 1.day))
|
||||
expect(widget.versions.length).to(eq(1))
|
||||
end
|
||||
|
||||
it "be nil in its previous version" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
expect(widget.versions.first.object).to(be_nil)
|
||||
expect(widget.versions.first.reify).to(be_nil)
|
||||
end
|
||||
|
||||
it "record the correct event" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
expect(widget.versions.first.event).to(match(/create/i))
|
||||
end
|
||||
|
||||
it "be live" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
expect(widget.paper_trail.live?).to(eq(true))
|
||||
end
|
||||
|
||||
it "use the widget `updated_at` as the version's `created_at`" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
expect(widget.versions.first.created_at.to_i).to(eq(widget.updated_at.to_i))
|
||||
end
|
||||
|
||||
context "when updated without any changes" do
|
||||
it "to have two previous versions" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.touch
|
||||
expect(widget.versions.length).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context "when updated with changes" do
|
||||
it "have three previous versions" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
expect(widget.versions.length).to(eq(2))
|
||||
end
|
||||
|
||||
it "be available in its previous version" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
expect(widget.name).to(eq("Harry"))
|
||||
expect(widget.versions.last.object).not_to(be_nil)
|
||||
reified_widget = widget.versions.last.reify
|
||||
expect(reified_widget.name).to(eq("Henry"))
|
||||
expect(widget.name).to(eq("Harry"))
|
||||
end
|
||||
|
||||
it "have the same ID in its previous version" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
expect(widget.versions.last.reify.id).to(eq(widget.id))
|
||||
end
|
||||
|
||||
it "record the correct event" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
expect(widget.versions.last.event).to(match(/update/i))
|
||||
end
|
||||
|
||||
it "have versions that are not live" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
widget.versions.map(&:reify).compact.each do |v|
|
||||
expect(v.paper_trail).not_to be_live
|
||||
end
|
||||
end
|
||||
|
||||
it "have stored changes" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
last_obj_changes = widget.versions.last.object_changes
|
||||
actual = PaperTrail.serializer.load(last_obj_changes).reject do |k, _v|
|
||||
(k.to_sym == :updated_at)
|
||||
end
|
||||
expect(actual).to(eq("name" => %w[Henry Harry]))
|
||||
actual = widget.versions.last.changeset.reject { |k, _v| (k.to_sym == :updated_at) }
|
||||
expect(actual).to(eq("name" => %w[Henry Harry]))
|
||||
end
|
||||
|
||||
it "return changes with indifferent access" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
expect(widget.versions.last.changeset[:name]).to(eq(%w[Henry Harry]))
|
||||
expect(widget.versions.last.changeset["name"]).to(eq(%w[Henry Harry]))
|
||||
end
|
||||
end
|
||||
|
||||
context "when updated, and has one associated object" do
|
||||
it "not copy the has_one association by default when reifying" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
wotsit = widget.create_wotsit name: "John"
|
||||
reified_widget = widget.versions.last.reify
|
||||
expect(reified_widget.wotsit).to eq(wotsit)
|
||||
expect(widget.reload.wotsit).to eq(wotsit)
|
||||
end
|
||||
end
|
||||
|
||||
context "when updated, and has many associated objects" do
|
||||
it "copy the has_many associations when reifying" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
widget.fluxors.create(name: "f-zero")
|
||||
widget.fluxors.create(name: "f-one")
|
||||
reified_widget = widget.versions.last.reify
|
||||
expect(reified_widget.fluxors.length).to(eq(widget.fluxors.length))
|
||||
expect(reified_widget.fluxors).to match_array(widget.fluxors)
|
||||
expect(reified_widget.versions.length).to(eq(widget.versions.length))
|
||||
expect(reified_widget.versions).to match_array(widget.versions)
|
||||
end
|
||||
end
|
||||
|
||||
context "when updated, and has many associated polymorphic objects" do
|
||||
it "copy the has_many associations when reifying" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
widget.whatchamajiggers.create(name: "f-zero")
|
||||
widget.whatchamajiggers.create(name: "f-zero")
|
||||
reified_widget = widget.versions.last.reify
|
||||
expect(reified_widget.whatchamajiggers.length).to eq(widget.whatchamajiggers.length)
|
||||
expect(reified_widget.whatchamajiggers).to match_array(widget.whatchamajiggers)
|
||||
expect(reified_widget.versions.length).to(eq(widget.versions.length))
|
||||
expect(reified_widget.versions).to match_array(widget.versions)
|
||||
end
|
||||
end
|
||||
|
||||
context "when updated, polymorphic objects by themselves" do
|
||||
it "not fail with a nil pointer on the polymorphic association" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
widget = Whatchamajigger.new(name: "f-zero")
|
||||
widget.save!
|
||||
end
|
||||
end
|
||||
|
||||
context "when updated, and then destroyed" do
|
||||
it "record the correct event" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
widget.destroy
|
||||
expect(PaperTrail::Version.last.event).to(match(/destroy/i))
|
||||
end
|
||||
|
||||
it "have three previous versions" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
widget.destroy
|
||||
expect(PaperTrail::Version.with_item_keys("Widget", widget.id).length).to(eq(3))
|
||||
end
|
||||
|
||||
it "returns the expected attributes for the reified widget" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
widget.destroy
|
||||
reified_widget = PaperTrail::Version.last.reify
|
||||
expect(reified_widget.id).to eq(widget.id)
|
||||
expected = widget.attributes
|
||||
actual = reified_widget.attributes
|
||||
expect(expected["id"]).to eq(actual["id"])
|
||||
expect(expected["name"]).to eq(actual["name"])
|
||||
expect(expected["a_text"]).to eq(actual["a_text"])
|
||||
expect(expected["an_integer"]).to eq(actual["an_integer"])
|
||||
expect(expected["a_float"]).to eq(actual["a_float"])
|
||||
expect(expected["a_decimal"]).to eq(actual["a_decimal"])
|
||||
expect(expected["a_datetime"]).to eq(actual["a_datetime"])
|
||||
expect(expected["a_time"]).to eq(actual["a_time"])
|
||||
expect(expected["a_date"]).to eq(actual["a_date"])
|
||||
expect(expected["a_boolean"]).to eq(actual["a_boolean"])
|
||||
expect(expected["type"]).to eq(actual["type"])
|
||||
|
||||
# We are using `to_i` to truncate to the nearest second, but isn't
|
||||
# there still a chance of this failing intermittently if
|
||||
# ___ and ___ occured more than 0.5s apart?
|
||||
expect(expected["created_at"].to_i).to eq(actual["created_at"].to_i)
|
||||
expect(expected["updated_at"].to_i).to eq(actual["updated_at"].to_i)
|
||||
end
|
||||
|
||||
it "be re-creatable from its previous version" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
widget.destroy
|
||||
reified_widget = PaperTrail::Version.last.reify
|
||||
expect(reified_widget.save).to(be_truthy)
|
||||
end
|
||||
|
||||
it "restore its associations on its previous version" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
widget.fluxors.create(name: "flux")
|
||||
widget.destroy
|
||||
reified_widget = PaperTrail::Version.last.reify
|
||||
reified_widget.save
|
||||
expect(reified_widget.fluxors.length).to(eq(1))
|
||||
end
|
||||
|
||||
it "have nil item for last version" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
widget.destroy
|
||||
expect(widget.versions.last.item).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with a record's papertrail" do
|
||||
let!(:d0) { Date.new(2009, 5, 29) }
|
||||
let!(:t0) { Time.current }
|
||||
let(:previous_widget) { widget.versions.last.reify }
|
||||
let(:widget) {
|
||||
Widget.create(
|
||||
name: "Warble",
|
||||
a_text: "The quick brown fox",
|
||||
an_integer: 42,
|
||||
a_float: 153.01,
|
||||
a_decimal: 2.71828,
|
||||
a_datetime: t0,
|
||||
a_time: t0,
|
||||
a_date: d0,
|
||||
a_boolean: true
|
||||
)
|
||||
}
|
||||
|
||||
before do
|
||||
widget.update(
|
||||
name: nil,
|
||||
a_text: nil,
|
||||
an_integer: nil,
|
||||
a_float: nil,
|
||||
a_decimal: nil,
|
||||
a_datetime: nil,
|
||||
a_time: nil,
|
||||
a_date: nil,
|
||||
a_boolean: false
|
||||
)
|
||||
end
|
||||
|
||||
it "handle strings" do
|
||||
expect(previous_widget.name).to(eq("Warble"))
|
||||
end
|
||||
|
||||
it "handle text" do
|
||||
expect(previous_widget.a_text).to(eq("The quick brown fox"))
|
||||
end
|
||||
|
||||
it "handle integers" do
|
||||
expect(previous_widget.an_integer).to(eq(42))
|
||||
end
|
||||
|
||||
it "handle floats" do
|
||||
assert_in_delta(153.01, previous_widget.a_float, 0.001)
|
||||
end
|
||||
|
||||
it "handle decimals" do
|
||||
assert_in_delta(2.7183, previous_widget.a_decimal, 0.0001)
|
||||
end
|
||||
|
||||
it "handle datetimes" do
|
||||
expect(previous_widget.a_datetime.to_time.utc.to_i).to(eq(t0.to_time.utc.to_i))
|
||||
end
|
||||
|
||||
it "handle times (time only, no date)" do
|
||||
format = ->(t) { t.utc.strftime "%H:%M:%S" }
|
||||
expect(format[previous_widget.a_time]).to eq(format[t0])
|
||||
end
|
||||
|
||||
it "handle dates" do
|
||||
expect(previous_widget.a_date).to(eq(d0))
|
||||
end
|
||||
|
||||
it "handle booleans" do
|
||||
expect(previous_widget.a_boolean).to(be_truthy)
|
||||
end
|
||||
|
||||
context "when a column has been removed from the record's schema" do
|
||||
let(:last_version) { widget.versions.last }
|
||||
|
||||
it "reify previous version" do
|
||||
assert_kind_of(Widget, last_version.reify)
|
||||
end
|
||||
|
||||
it "restore all forward-compatible attributes" do
|
||||
reified = last_version.reify
|
||||
expect(reified.name).to(eq("Warble"))
|
||||
expect(reified.a_text).to(eq("The quick brown fox"))
|
||||
expect(reified.an_integer).to(eq(42))
|
||||
assert_in_delta(153.01, reified.a_float, 0.001)
|
||||
assert_in_delta(2.7183, reified.a_decimal, 0.0001)
|
||||
expect(reified.a_datetime.to_time.utc.to_i).to(eq(t0.to_time.utc.to_i))
|
||||
format = ->(t) { t.utc.strftime "%H:%M:%S" }
|
||||
expect(format[reified.a_time]).to eq(format[t0])
|
||||
expect(reified.a_date).to(eq(d0))
|
||||
expect(reified.a_boolean).to(be_truthy)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with a record" do
|
||||
context "with PaperTrail globally disabled, when updated" do
|
||||
after { PaperTrail.enabled = true }
|
||||
|
||||
it "not add to its trail" do
|
||||
widget = Widget.create(name: "Zaphod")
|
||||
PaperTrail.enabled = false
|
||||
count = widget.versions.length
|
||||
widget.update(name: "Beeblebrox")
|
||||
expect(widget.versions.length).to(eq(count))
|
||||
end
|
||||
end
|
||||
|
||||
context "with its paper trail turned off, when updated" do
|
||||
after do
|
||||
PaperTrail.request.enable_model(Widget)
|
||||
end
|
||||
|
||||
it "not add to its trail" do
|
||||
widget = Widget.create(name: "Zaphod")
|
||||
PaperTrail.request.disable_model(Widget)
|
||||
count = widget.versions.length
|
||||
widget.update(name: "Beeblebrox")
|
||||
expect(widget.versions.length).to(eq(count))
|
||||
end
|
||||
|
||||
it "add to its trail" do
|
||||
widget = Widget.create(name: "Zaphod")
|
||||
PaperTrail.request.disable_model(Widget)
|
||||
count = widget.versions.length
|
||||
widget.update(name: "Beeblebrox")
|
||||
PaperTrail.request.enable_model(Widget)
|
||||
widget.update(name: "Ford")
|
||||
expect(widget.versions.length).to(eq((count + 1)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with somebody making changes" do
|
||||
context "when a record is created" do
|
||||
it "tracks who made the change" do
|
||||
widget = Widget.new(name: "Fidget")
|
||||
PaperTrail.request.whodunnit = "Alice"
|
||||
widget.save
|
||||
version = widget.versions.last
|
||||
expect(version.whodunnit).to(eq("Alice"))
|
||||
expect(version.paper_trail_originator).to(be_nil)
|
||||
expect(version.terminator).to(eq("Alice"))
|
||||
expect(widget.paper_trail.originator).to(eq("Alice"))
|
||||
end
|
||||
end
|
||||
|
||||
context "when created, then updated" do
|
||||
it "tracks who made the change" do
|
||||
widget = Widget.new(name: "Fidget")
|
||||
PaperTrail.request.whodunnit = "Alice"
|
||||
widget.save
|
||||
PaperTrail.request.whodunnit = "Bob"
|
||||
widget.update(name: "Rivet")
|
||||
version = widget.versions.last
|
||||
expect(version.whodunnit).to(eq("Bob"))
|
||||
expect(version.paper_trail_originator).to(eq("Alice"))
|
||||
expect(version.terminator).to(eq("Bob"))
|
||||
expect(widget.paper_trail.originator).to(eq("Bob"))
|
||||
end
|
||||
end
|
||||
|
||||
context "when created, updated, and destroyed" do
|
||||
it "tracks who made the change" do
|
||||
widget = Widget.new(name: "Fidget")
|
||||
PaperTrail.request.whodunnit = "Alice"
|
||||
widget.save
|
||||
PaperTrail.request.whodunnit = "Bob"
|
||||
widget.update(name: "Rivet")
|
||||
PaperTrail.request.whodunnit = "Charlie"
|
||||
widget.destroy
|
||||
version = PaperTrail::Version.last
|
||||
expect(version.whodunnit).to(eq("Charlie"))
|
||||
expect(version.paper_trail_originator).to(eq("Bob"))
|
||||
expect(version.terminator).to(eq("Charlie"))
|
||||
expect(widget.paper_trail.originator).to(eq("Charlie"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with an item with versions" do
|
||||
context "when the versions were created over time" do
|
||||
let(:widget) { Widget.create(name: "Widget") }
|
||||
let(:t0) { 2.days.ago }
|
||||
let(:t1) { 1.day.ago }
|
||||
let(:t2) { 1.hour.ago }
|
||||
|
||||
before do
|
||||
widget.update(name: "Fidget")
|
||||
widget.update(name: "Digit")
|
||||
widget.versions[0].update(created_at: t0)
|
||||
widget.versions[1].update(created_at: t1)
|
||||
widget.versions[2].update(created_at: t2)
|
||||
widget.update_attribute(:updated_at, t2)
|
||||
end
|
||||
|
||||
it "return nil for version_at before it was created" do
|
||||
expect(widget.paper_trail.version_at((t0 - 1))).to(be_nil)
|
||||
end
|
||||
|
||||
it "return how it looked when created for version_at its creation" do
|
||||
expect(widget.paper_trail.version_at(t0).name).to(eq("Widget"))
|
||||
end
|
||||
|
||||
it "return how it looked before its first update" do
|
||||
expect(widget.paper_trail.version_at((t1 - 1)).name).to(eq("Widget"))
|
||||
end
|
||||
|
||||
it "return how it looked after its first update" do
|
||||
expect(widget.paper_trail.version_at(t1).name).to(eq("Fidget"))
|
||||
end
|
||||
|
||||
it "return how it looked before its second update" do
|
||||
expect(widget.paper_trail.version_at((t2 - 1)).name).to(eq("Fidget"))
|
||||
end
|
||||
|
||||
it "return how it looked after its second update" do
|
||||
expect(widget.paper_trail.version_at(t2).name).to(eq("Digit"))
|
||||
end
|
||||
|
||||
it "return the current object for version_at after latest update" do
|
||||
expect(widget.paper_trail.version_at(1.day.from_now).name).to(eq("Digit"))
|
||||
end
|
||||
|
||||
it "still return a widget when appropriate, when passing timestamp as string" do
|
||||
expect(
|
||||
widget.paper_trail.version_at((t0 + 1.second).to_s).name
|
||||
).to(eq("Widget"))
|
||||
expect(
|
||||
widget.paper_trail.version_at((t1 + 1.second).to_s).name
|
||||
).to(eq("Fidget"))
|
||||
expect(
|
||||
widget.paper_trail.version_at((t2 + 1.second).to_s).name
|
||||
).to(eq("Digit"))
|
||||
end
|
||||
end
|
||||
|
||||
describe ".versions_between" do
|
||||
it "return versions in the time period" do
|
||||
widget = Widget.create(name: "Widget")
|
||||
widget.update(name: "Fidget")
|
||||
widget.update(name: "Digit")
|
||||
widget.versions[0].update(created_at: 30.days.ago)
|
||||
widget.versions[1].update(created_at: 15.days.ago)
|
||||
widget.versions[2].update(created_at: 1.day.ago)
|
||||
widget.update_attribute(:updated_at, 1.day.ago)
|
||||
expect(
|
||||
widget.paper_trail.versions_between(20.days.ago, 10.days.ago).map(&:name)
|
||||
).to(eq(["Fidget"]))
|
||||
expect(
|
||||
widget.paper_trail.versions_between(45.days.ago, 10.days.ago).map(&:name)
|
||||
).to(eq(%w[Widget Fidget]))
|
||||
expect(
|
||||
widget.paper_trail.versions_between(16.days.ago, 1.minute.ago).map(&:name)
|
||||
).to(eq(%w[Fidget Digit Digit]))
|
||||
expect(
|
||||
widget.paper_trail.versions_between(60.days.ago, 45.days.ago).map(&:name)
|
||||
).to(eq([]))
|
||||
end
|
||||
end
|
||||
|
||||
context "with the first version" do
|
||||
let(:widget) { Widget.create(name: "Widget") }
|
||||
let(:version) { widget.versions.last }
|
||||
|
||||
before do
|
||||
widget = Widget.create(name: "Widget")
|
||||
widget.update(name: "Fidget")
|
||||
widget.update(name: "Digit")
|
||||
end
|
||||
|
||||
it "have a nil previous version" do
|
||||
expect(version.previous).to(be_nil)
|
||||
end
|
||||
|
||||
it "return the next version" do
|
||||
expect(version.next).to(eq(widget.versions[1]))
|
||||
end
|
||||
|
||||
it "return the correct index" do
|
||||
expect(version.index).to(eq(0))
|
||||
end
|
||||
end
|
||||
|
||||
context "with the last version" do
|
||||
let(:widget) { Widget.create(name: "Widget") }
|
||||
let(:version) { widget.versions.last }
|
||||
|
||||
before do
|
||||
widget.update(name: "Fidget")
|
||||
widget.update(name: "Digit")
|
||||
end
|
||||
|
||||
it "return the previous version" do
|
||||
expect(version.previous).to(eq(widget.versions[(widget.versions.length - 2)]))
|
||||
end
|
||||
|
||||
it "have a nil next version" do
|
||||
expect(version.next).to(be_nil)
|
||||
end
|
||||
|
||||
it "return the correct index" do
|
||||
expect(version.index).to(eq((widget.versions.length - 1)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with a reified item" do
|
||||
it "know which version it came from, and return its previous self" do
|
||||
widget = Widget.create(name: "Bob")
|
||||
%w[Tom Dick Jane].each do |name|
|
||||
widget.update(name: name)
|
||||
end
|
||||
version = widget.versions.last
|
||||
widget = version.reify
|
||||
expect(widget.version).to(eq(version))
|
||||
expect(widget.paper_trail.previous_version).to(eq(widget.versions[-2].reify))
|
||||
end
|
||||
end
|
||||
|
||||
describe "#next_version" do
|
||||
context "with a reified item" do
|
||||
it "returns the object (not a Version) as it became next" do
|
||||
widget = Widget.create(name: "Bob")
|
||||
%w[Tom Dick Jane].each do |name|
|
||||
widget.update(name: name)
|
||||
end
|
||||
second_widget = widget.versions[1].reify
|
||||
last_widget = widget.versions.last.reify
|
||||
expect(second_widget.paper_trail.next_version.name).to(eq(widget.versions[2].reify.name))
|
||||
expect(widget.name).to(eq(last_widget.paper_trail.next_version.name))
|
||||
end
|
||||
end
|
||||
|
||||
context "with a non-reified item" do
|
||||
it "always returns nil because cannot ever have a next version" do
|
||||
widget = Widget.new
|
||||
expect(widget.paper_trail.next_version).to(be_nil)
|
||||
widget.save
|
||||
%w[Tom Dick Jane].each do |name|
|
||||
widget.update(name: name)
|
||||
end
|
||||
expect(widget.paper_trail.next_version).to(be_nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#previous_version" do
|
||||
context "with a reified item" do
|
||||
it "returns the object (not a Version) as it was most recently" do
|
||||
widget = Widget.create(name: "Bob")
|
||||
%w[Tom Dick Jane].each do |name|
|
||||
widget.update(name: name)
|
||||
end
|
||||
second_widget = widget.versions[1].reify
|
||||
last_widget = widget.versions.last.reify
|
||||
expect(second_widget.paper_trail.previous_version).to(be_nil)
|
||||
expect(last_widget.paper_trail.previous_version.name).to(eq(widget.versions[-2].reify.name))
|
||||
end
|
||||
end
|
||||
|
||||
context "with a non-reified item" do
|
||||
it "returns the object (not a Version) as it was most recently" do
|
||||
widget = Widget.new
|
||||
expect(widget.paper_trail.previous_version).to(be_nil)
|
||||
widget.save
|
||||
%w[Tom Dick Jane].each do |name|
|
||||
widget.update(name: name)
|
||||
end
|
||||
expect(widget.paper_trail.previous_version.name).to(eq(widget.versions.last.reify.name))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with an unsaved record" do
|
||||
it "not have a version created on destroy" do
|
||||
widget = Widget.new
|
||||
widget.destroy
|
||||
expect(widget.versions.empty?).to(eq(true))
|
||||
end
|
||||
end
|
||||
|
||||
context "when measuring the memory allocation of" do
|
||||
let(:widget) do
|
||||
Widget.new(
|
||||
name: "Warble",
|
||||
a_text: "The quick brown fox",
|
||||
an_integer: 42,
|
||||
a_float: 153.01,
|
||||
a_decimal: 2.71828,
|
||||
a_boolean: true
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
# Json fields for `object` & `object_changes` attributes is most efficient way
|
||||
# to do the things - this way we will save even more RAM, as well as will skip
|
||||
# the whole YAML serialization
|
||||
allow(PaperTrail::Version).to receive(:object_changes_col_is_json?).and_return(true)
|
||||
allow(PaperTrail::Version).to receive(:object_col_is_json?).and_return(true)
|
||||
|
||||
# Force the loading of all lazy things like class definitions,
|
||||
# in order to get the pure benchmark
|
||||
version_building.call
|
||||
end
|
||||
|
||||
describe "#build_version_on_create" do
|
||||
let(:version_building) do
|
||||
lambda do
|
||||
widget.paper_trail.send(
|
||||
:build_version_on_create,
|
||||
in_after_callback: false
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it "is frugal enough" do
|
||||
# Some time ago there was 95kbs..
|
||||
# At the time of commit the test passes with assertion on 17kbs.
|
||||
# Lets assert 20kbs then, to avoid flaky fails.
|
||||
expect(&version_building).to allocate_less_than(20).kilobytes
|
||||
end
|
||||
end
|
||||
|
||||
describe "#build_version_on_update" do
|
||||
let(:widget) do
|
||||
super().tap do |w|
|
||||
w.save!
|
||||
w.attributes = {
|
||||
name: "Dostoyevsky",
|
||||
a_text: "The slow yellow mouse",
|
||||
an_integer: 84,
|
||||
a_float: 306.02,
|
||||
a_decimal: 5.43656,
|
||||
a_boolean: false
|
||||
}
|
||||
end
|
||||
end
|
||||
let(:version_building) do
|
||||
lambda do
|
||||
widget.paper_trail.send(
|
||||
:build_version_on_update,
|
||||
force: false,
|
||||
in_after_callback: false,
|
||||
is_touch: false
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it "is frugal enough" do
|
||||
# Some time ago there was 144kbs..
|
||||
# At the time of commit the test passes with assertion on 27kbs.
|
||||
# Lets assert 35kbs then, to avoid flaky fails.
|
||||
expect(&version_building).to allocate_less_than(35).kilobytes
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "`be_versioned` matcher" do
|
||||
it { is_expected.to be_versioned }
|
||||
end
|
||||
|
||||
describe "`have_a_version_with` matcher", versioning: true do
|
||||
let(:widget) { Widget.create! name: "Bob", an_integer: 1 }
|
||||
|
||||
before do
|
||||
widget.update!(name: "Leonard", an_integer: 1)
|
||||
widget.update!(name: "Tom")
|
||||
|
@ -26,18 +743,22 @@ RSpec.describe Widget, type: :model do
|
|||
describe "versioning option" do
|
||||
context "when enabled", versioning: true do
|
||||
it "enables versioning" do
|
||||
widget = Widget.create! name: "Bob", an_integer: 1
|
||||
expect(widget.versions.size).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context "when disabled (default)" do
|
||||
context "when disabled", versioning: false do
|
||||
it "does not enable versioning" do
|
||||
widget = Widget.create! name: "Bob", an_integer: 1
|
||||
expect(widget.versions.size).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "Callbacks", versioning: true do
|
||||
let(:widget) { Widget.create! name: "Bob", an_integer: 1 }
|
||||
|
||||
describe "before_save" do
|
||||
it "resets value for timestamp attrs for update so that value gets updated properly" do
|
||||
widget.update!(name: "Foobar")
|
||||
|
@ -111,6 +832,8 @@ RSpec.describe Widget, type: :model do
|
|||
end
|
||||
|
||||
describe "Association", versioning: true do
|
||||
let(:widget) { Widget.create! name: "Bob", an_integer: 1 }
|
||||
|
||||
describe "sort order" do
|
||||
it "sorts by the timestamp order from the `VersionConcern`" do
|
||||
expect(widget.versions.to_sql).to eq(
|
||||
|
@ -120,6 +843,7 @@ RSpec.describe Widget, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
# TODO: I think IdentityMap no longer exists
|
||||
if defined?(ActiveRecord::IdentityMap) && ActiveRecord::IdentityMap.respond_to?(:without)
|
||||
describe "IdentityMap", versioning: true do
|
||||
it "does not clobber the IdentityMap when reifying" do
|
||||
|
@ -133,6 +857,8 @@ RSpec.describe Widget, type: :model do
|
|||
end
|
||||
|
||||
describe "#create", versioning: true do
|
||||
let(:widget) { Widget.create! name: "Bob", an_integer: 1 }
|
||||
|
||||
it "creates a version record" do
|
||||
wordget = Widget.create
|
||||
assert_equal 1, wordget.versions.length
|
||||
|
@ -140,6 +866,8 @@ RSpec.describe Widget, type: :model do
|
|||
end
|
||||
|
||||
describe "#destroy", versioning: true do
|
||||
let(:widget) { Widget.create! name: "Bob", an_integer: 1 }
|
||||
|
||||
it "creates a version record" do
|
||||
widget = Widget.create
|
||||
assert_equal 1, widget.versions.length
|
||||
|
@ -168,6 +896,8 @@ RSpec.describe Widget, type: :model do
|
|||
end
|
||||
|
||||
describe "#paper_trail.originator", versioning: true do
|
||||
let(:widget) { Widget.create! name: "Bob", an_integer: 1 }
|
||||
|
||||
describe "return value" do
|
||||
let(:orig_name) { FFaker::Name.name }
|
||||
let(:new_name) { FFaker::Name.name }
|
||||
|
@ -206,6 +936,8 @@ RSpec.describe Widget, type: :model do
|
|||
end
|
||||
|
||||
describe "#version_at", versioning: true do
|
||||
let(:widget) { Widget.create! name: "Bob", an_integer: 1 }
|
||||
|
||||
context "when Timestamp argument is AFTER object has been destroyed" do
|
||||
it "returns nil" do
|
||||
widget.update_attribute(:name, "foobar")
|
||||
|
@ -216,6 +948,8 @@ RSpec.describe Widget, type: :model do
|
|||
end
|
||||
|
||||
describe "touch", versioning: true do
|
||||
let(:widget) { Widget.create! name: "Bob", an_integer: 1 }
|
||||
|
||||
it "creates a version" do
|
||||
expect { widget.touch }.to change {
|
||||
widget.versions.count
|
||||
|
@ -234,6 +968,8 @@ RSpec.describe Widget, type: :model do
|
|||
end
|
||||
|
||||
describe ".paper_trail.update_columns", versioning: true do
|
||||
let(:widget) { Widget.create! name: "Bob", an_integer: 1 }
|
||||
|
||||
it "creates a version record" do
|
||||
widget = Widget.create
|
||||
expect(widget.versions.count).to eq(1)
|
||||
|
@ -245,6 +981,8 @@ RSpec.describe Widget, type: :model do
|
|||
end
|
||||
|
||||
describe "#update", versioning: true do
|
||||
let(:widget) { Widget.create! name: "Bob", an_integer: 1 }
|
||||
|
||||
it "creates a version record" do
|
||||
widget = Widget.create
|
||||
assert_equal 1, widget.versions.length
|
||||
|
|
18
spec/models/wotsit_spec.rb
Normal file
18
spec/models/wotsit_spec.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe Wotsit, versioning: true do
|
||||
it "update! records timestamps" do
|
||||
wotsit = Wotsit.create!(name: "wotsit")
|
||||
wotsit.update!(name: "changed")
|
||||
reified = wotsit.versions.last.reify
|
||||
expect(reified.created_at).not_to(be_nil)
|
||||
expect(reified.updated_at).not_to(be_nil)
|
||||
end
|
||||
|
||||
it "update! does not raise error" do
|
||||
wotsit = Wotsit.create!(name: "name1")
|
||||
expect { wotsit.update!(name: "name2") }.not_to(raise_error)
|
||||
end
|
||||
end
|
|
@ -1,922 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
require "support/performance_helpers"
|
||||
|
||||
RSpec.describe(::PaperTrail, versioning: true) do
|
||||
describe "#changeset" do
|
||||
it "has expected values" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
changeset = widget.versions.last.changeset
|
||||
expect(changeset["name"]).to eq([nil, "Henry"])
|
||||
expect(changeset["id"]).to eq([nil, widget.id])
|
||||
# When comparing timestamps, round off to the nearest second, because
|
||||
# mysql doesn't do fractional seconds.
|
||||
expect(changeset["created_at"][0]).to be_nil
|
||||
expect(changeset["created_at"][1].to_i).to eq(widget.created_at.to_i)
|
||||
expect(changeset["updated_at"][0]).to be_nil
|
||||
expect(changeset["updated_at"][1].to_i).to eq(widget.updated_at.to_i)
|
||||
end
|
||||
|
||||
context "with custom object_changes_adapter" do
|
||||
after do
|
||||
PaperTrail.config.object_changes_adapter = nil
|
||||
end
|
||||
|
||||
it "calls the adapter's load_changeset method" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
adapter = instance_spy("CustomObjectChangesAdapter")
|
||||
PaperTrail.config.object_changes_adapter = adapter
|
||||
allow(adapter).to(
|
||||
receive(:load_changeset).with(widget.versions.last).and_return(a: "b", c: "d")
|
||||
)
|
||||
changeset = widget.versions.last.changeset
|
||||
expect(changeset[:a]).to eq("b")
|
||||
expect(changeset[:c]).to eq("d")
|
||||
expect(adapter).to have_received(:load_changeset)
|
||||
end
|
||||
|
||||
it "defaults to the original behavior" do
|
||||
adapter = Class.new.new
|
||||
PaperTrail.config.object_changes_adapter = adapter
|
||||
widget = Widget.create(name: "Henry")
|
||||
changeset = widget.versions.last.changeset
|
||||
expect(changeset[:name]).to eq([nil, "Henry"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with a new record" do
|
||||
it "not have any previous versions" do
|
||||
expect(Widget.new.versions).to(eq([]))
|
||||
end
|
||||
|
||||
it "be live" do
|
||||
expect(Widget.new.paper_trail.live?).to(eq(true))
|
||||
end
|
||||
end
|
||||
|
||||
context "with a persisted record" do
|
||||
it "have one previous version" do
|
||||
widget = Widget.create(name: "Henry", created_at: (Time.current - 1.day))
|
||||
expect(widget.versions.length).to(eq(1))
|
||||
end
|
||||
|
||||
it "be nil in its previous version" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
expect(widget.versions.first.object).to(be_nil)
|
||||
expect(widget.versions.first.reify).to(be_nil)
|
||||
end
|
||||
|
||||
it "record the correct event" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
expect(widget.versions.first.event).to(match(/create/i))
|
||||
end
|
||||
|
||||
it "be live" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
expect(widget.paper_trail.live?).to(eq(true))
|
||||
end
|
||||
|
||||
it "use the widget `updated_at` as the version's `created_at`" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
expect(widget.versions.first.created_at.to_i).to(eq(widget.updated_at.to_i))
|
||||
end
|
||||
|
||||
context "when updated without any changes" do
|
||||
it "to have two previous versions" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.touch
|
||||
expect(widget.versions.length).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context "when updated with changes" do
|
||||
it "have three previous versions" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
expect(widget.versions.length).to(eq(2))
|
||||
end
|
||||
|
||||
it "be available in its previous version" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
expect(widget.name).to(eq("Harry"))
|
||||
expect(widget.versions.last.object).not_to(be_nil)
|
||||
reified_widget = widget.versions.last.reify
|
||||
expect(reified_widget.name).to(eq("Henry"))
|
||||
expect(widget.name).to(eq("Harry"))
|
||||
end
|
||||
|
||||
it "have the same ID in its previous version" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
expect(widget.versions.last.reify.id).to(eq(widget.id))
|
||||
end
|
||||
|
||||
it "record the correct event" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
expect(widget.versions.last.event).to(match(/update/i))
|
||||
end
|
||||
|
||||
it "have versions that are not live" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
widget.versions.map(&:reify).compact.each do |v|
|
||||
expect(v.paper_trail).not_to be_live
|
||||
end
|
||||
end
|
||||
|
||||
it "have stored changes" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
last_obj_changes = widget.versions.last.object_changes
|
||||
actual = PaperTrail.serializer.load(last_obj_changes).reject do |k, _v|
|
||||
(k.to_sym == :updated_at)
|
||||
end
|
||||
expect(actual).to(eq("name" => %w[Henry Harry]))
|
||||
actual = widget.versions.last.changeset.reject { |k, _v| (k.to_sym == :updated_at) }
|
||||
expect(actual).to(eq("name" => %w[Henry Harry]))
|
||||
end
|
||||
|
||||
it "return changes with indifferent access" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
expect(widget.versions.last.changeset[:name]).to(eq(%w[Henry Harry]))
|
||||
expect(widget.versions.last.changeset["name"]).to(eq(%w[Henry Harry]))
|
||||
end
|
||||
end
|
||||
|
||||
context "when updated, and has one associated object" do
|
||||
it "not copy the has_one association by default when reifying" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
wotsit = widget.create_wotsit name: "John"
|
||||
reified_widget = widget.versions.last.reify
|
||||
expect(reified_widget.wotsit).to eq(wotsit)
|
||||
expect(widget.reload.wotsit).to eq(wotsit)
|
||||
end
|
||||
end
|
||||
|
||||
context "when updated, and has many associated objects" do
|
||||
it "copy the has_many associations when reifying" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
widget.fluxors.create(name: "f-zero")
|
||||
widget.fluxors.create(name: "f-one")
|
||||
reified_widget = widget.versions.last.reify
|
||||
expect(reified_widget.fluxors.length).to(eq(widget.fluxors.length))
|
||||
expect(reified_widget.fluxors).to match_array(widget.fluxors)
|
||||
expect(reified_widget.versions.length).to(eq(widget.versions.length))
|
||||
expect(reified_widget.versions).to match_array(widget.versions)
|
||||
end
|
||||
end
|
||||
|
||||
context "when updated, and has many associated polymorphic objects" do
|
||||
it "copy the has_many associations when reifying" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
widget.whatchamajiggers.create(name: "f-zero")
|
||||
widget.whatchamajiggers.create(name: "f-zero")
|
||||
reified_widget = widget.versions.last.reify
|
||||
expect(reified_widget.whatchamajiggers.length).to eq(widget.whatchamajiggers.length)
|
||||
expect(reified_widget.whatchamajiggers).to match_array(widget.whatchamajiggers)
|
||||
expect(reified_widget.versions.length).to(eq(widget.versions.length))
|
||||
expect(reified_widget.versions).to match_array(widget.versions)
|
||||
end
|
||||
end
|
||||
|
||||
context "when updated, polymorphic objects by themselves" do
|
||||
it "not fail with a nil pointer on the polymorphic association" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
widget = Whatchamajigger.new(name: "f-zero")
|
||||
widget.save!
|
||||
end
|
||||
end
|
||||
|
||||
context "when updated, and then destroyed" do
|
||||
it "record the correct event" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
widget.destroy
|
||||
expect(PaperTrail::Version.last.event).to(match(/destroy/i))
|
||||
end
|
||||
|
||||
it "have three previous versions" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
widget.destroy
|
||||
expect(PaperTrail::Version.with_item_keys("Widget", widget.id).length).to(eq(3))
|
||||
end
|
||||
|
||||
it "returns the expected attributes for the reified widget" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
widget.destroy
|
||||
reified_widget = PaperTrail::Version.last.reify
|
||||
expect(reified_widget.id).to eq(widget.id)
|
||||
expected = widget.attributes
|
||||
actual = reified_widget.attributes
|
||||
expect(expected["id"]).to eq(actual["id"])
|
||||
expect(expected["name"]).to eq(actual["name"])
|
||||
expect(expected["a_text"]).to eq(actual["a_text"])
|
||||
expect(expected["an_integer"]).to eq(actual["an_integer"])
|
||||
expect(expected["a_float"]).to eq(actual["a_float"])
|
||||
expect(expected["a_decimal"]).to eq(actual["a_decimal"])
|
||||
expect(expected["a_datetime"]).to eq(actual["a_datetime"])
|
||||
expect(expected["a_time"]).to eq(actual["a_time"])
|
||||
expect(expected["a_date"]).to eq(actual["a_date"])
|
||||
expect(expected["a_boolean"]).to eq(actual["a_boolean"])
|
||||
expect(expected["type"]).to eq(actual["type"])
|
||||
|
||||
# We are using `to_i` to truncate to the nearest second, but isn't
|
||||
# there still a chance of this failing intermittently if
|
||||
# ___ and ___ occured more than 0.5s apart?
|
||||
expect(expected["created_at"].to_i).to eq(actual["created_at"].to_i)
|
||||
expect(expected["updated_at"].to_i).to eq(actual["updated_at"].to_i)
|
||||
end
|
||||
|
||||
it "be re-creatable from its previous version" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
widget.destroy
|
||||
reified_widget = PaperTrail::Version.last.reify
|
||||
expect(reified_widget.save).to(be_truthy)
|
||||
end
|
||||
|
||||
it "restore its associations on its previous version" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
widget.fluxors.create(name: "flux")
|
||||
widget.destroy
|
||||
reified_widget = PaperTrail::Version.last.reify
|
||||
reified_widget.save
|
||||
expect(reified_widget.fluxors.length).to(eq(1))
|
||||
end
|
||||
|
||||
it "have nil item for last version" do
|
||||
widget = Widget.create(name: "Henry")
|
||||
widget.update(name: "Harry")
|
||||
widget.destroy
|
||||
expect(widget.versions.last.item).to be_nil
|
||||
end
|
||||
|
||||
it "has changes" do
|
||||
book = Book.create! title: "A"
|
||||
changes = YAML.load book.versions.last.attributes["object_changes"]
|
||||
expect(changes).to eq("id" => [nil, book.id], "title" => [nil, "A"])
|
||||
|
||||
book.update! title: "B"
|
||||
changes = YAML.load book.versions.last.attributes["object_changes"]
|
||||
expect(changes).to eq("title" => %w[A B])
|
||||
|
||||
book.destroy
|
||||
changes = YAML.load book.versions.last.attributes["object_changes"]
|
||||
expect(changes).to eq("id" => [book.id, nil], "title" => ["B", nil])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with a record's papertrail" do
|
||||
let!(:d0) { Date.new(2009, 5, 29) }
|
||||
let!(:t0) { Time.current }
|
||||
let(:previous_widget) { widget.versions.last.reify }
|
||||
let(:widget) {
|
||||
Widget.create(
|
||||
name: "Warble",
|
||||
a_text: "The quick brown fox",
|
||||
an_integer: 42,
|
||||
a_float: 153.01,
|
||||
a_decimal: 2.71828,
|
||||
a_datetime: t0,
|
||||
a_time: t0,
|
||||
a_date: d0,
|
||||
a_boolean: true
|
||||
)
|
||||
}
|
||||
|
||||
before do
|
||||
widget.update(
|
||||
name: nil,
|
||||
a_text: nil,
|
||||
an_integer: nil,
|
||||
a_float: nil,
|
||||
a_decimal: nil,
|
||||
a_datetime: nil,
|
||||
a_time: nil,
|
||||
a_date: nil,
|
||||
a_boolean: false
|
||||
)
|
||||
end
|
||||
|
||||
it "handle strings" do
|
||||
expect(previous_widget.name).to(eq("Warble"))
|
||||
end
|
||||
|
||||
it "handle text" do
|
||||
expect(previous_widget.a_text).to(eq("The quick brown fox"))
|
||||
end
|
||||
|
||||
it "handle integers" do
|
||||
expect(previous_widget.an_integer).to(eq(42))
|
||||
end
|
||||
|
||||
it "handle floats" do
|
||||
assert_in_delta(153.01, previous_widget.a_float, 0.001)
|
||||
end
|
||||
|
||||
it "handle decimals" do
|
||||
assert_in_delta(2.7183, previous_widget.a_decimal, 0.0001)
|
||||
end
|
||||
|
||||
it "handle datetimes" do
|
||||
expect(previous_widget.a_datetime.to_time.utc.to_i).to(eq(t0.to_time.utc.to_i))
|
||||
end
|
||||
|
||||
it "handle times (time only, no date)" do
|
||||
format = ->(t) { t.utc.strftime "%H:%M:%S" }
|
||||
expect(format[previous_widget.a_time]).to eq(format[t0])
|
||||
end
|
||||
|
||||
it "handle dates" do
|
||||
expect(previous_widget.a_date).to(eq(d0))
|
||||
end
|
||||
|
||||
it "handle booleans" do
|
||||
expect(previous_widget.a_boolean).to(be_truthy)
|
||||
end
|
||||
|
||||
context "when a column has been removed from the record's schema" do
|
||||
let(:last_version) { widget.versions.last }
|
||||
|
||||
it "reify previous version" do
|
||||
assert_kind_of(Widget, last_version.reify)
|
||||
end
|
||||
|
||||
it "restore all forward-compatible attributes" do
|
||||
reified = last_version.reify
|
||||
expect(reified.name).to(eq("Warble"))
|
||||
expect(reified.a_text).to(eq("The quick brown fox"))
|
||||
expect(reified.an_integer).to(eq(42))
|
||||
assert_in_delta(153.01, reified.a_float, 0.001)
|
||||
assert_in_delta(2.7183, reified.a_decimal, 0.0001)
|
||||
expect(reified.a_datetime.to_time.utc.to_i).to(eq(t0.to_time.utc.to_i))
|
||||
format = ->(t) { t.utc.strftime "%H:%M:%S" }
|
||||
expect(format[reified.a_time]).to eq(format[t0])
|
||||
expect(reified.a_date).to(eq(d0))
|
||||
expect(reified.a_boolean).to(be_truthy)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with a record" do
|
||||
context "with PaperTrail globally disabled, when updated" do
|
||||
after { PaperTrail.enabled = true }
|
||||
|
||||
it "not add to its trail" do
|
||||
widget = Widget.create(name: "Zaphod")
|
||||
PaperTrail.enabled = false
|
||||
count = widget.versions.length
|
||||
widget.update(name: "Beeblebrox")
|
||||
expect(widget.versions.length).to(eq(count))
|
||||
end
|
||||
end
|
||||
|
||||
context "with its paper trail turned off, when updated" do
|
||||
after do
|
||||
PaperTrail.request.enable_model(Widget)
|
||||
end
|
||||
|
||||
it "not add to its trail" do
|
||||
widget = Widget.create(name: "Zaphod")
|
||||
PaperTrail.request.disable_model(Widget)
|
||||
count = widget.versions.length
|
||||
widget.update(name: "Beeblebrox")
|
||||
expect(widget.versions.length).to(eq(count))
|
||||
end
|
||||
|
||||
it "add to its trail" do
|
||||
widget = Widget.create(name: "Zaphod")
|
||||
PaperTrail.request.disable_model(Widget)
|
||||
count = widget.versions.length
|
||||
widget.update(name: "Beeblebrox")
|
||||
PaperTrail.request.enable_model(Widget)
|
||||
widget.update(name: "Ford")
|
||||
expect(widget.versions.length).to(eq((count + 1)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with somebody making changes" do
|
||||
context "when a record is created" do
|
||||
it "tracks who made the change" do
|
||||
widget = Widget.new(name: "Fidget")
|
||||
PaperTrail.request.whodunnit = "Alice"
|
||||
widget.save
|
||||
version = widget.versions.last
|
||||
expect(version.whodunnit).to(eq("Alice"))
|
||||
expect(version.paper_trail_originator).to(be_nil)
|
||||
expect(version.terminator).to(eq("Alice"))
|
||||
expect(widget.paper_trail.originator).to(eq("Alice"))
|
||||
end
|
||||
end
|
||||
|
||||
context "when created, then updated" do
|
||||
it "tracks who made the change" do
|
||||
widget = Widget.new(name: "Fidget")
|
||||
PaperTrail.request.whodunnit = "Alice"
|
||||
widget.save
|
||||
PaperTrail.request.whodunnit = "Bob"
|
||||
widget.update(name: "Rivet")
|
||||
version = widget.versions.last
|
||||
expect(version.whodunnit).to(eq("Bob"))
|
||||
expect(version.paper_trail_originator).to(eq("Alice"))
|
||||
expect(version.terminator).to(eq("Bob"))
|
||||
expect(widget.paper_trail.originator).to(eq("Bob"))
|
||||
end
|
||||
end
|
||||
|
||||
context "when created, updated, and destroyed" do
|
||||
it "tracks who made the change" do
|
||||
widget = Widget.new(name: "Fidget")
|
||||
PaperTrail.request.whodunnit = "Alice"
|
||||
widget.save
|
||||
PaperTrail.request.whodunnit = "Bob"
|
||||
widget.update(name: "Rivet")
|
||||
PaperTrail.request.whodunnit = "Charlie"
|
||||
widget.destroy
|
||||
version = PaperTrail::Version.last
|
||||
expect(version.whodunnit).to(eq("Charlie"))
|
||||
expect(version.paper_trail_originator).to(eq("Bob"))
|
||||
expect(version.terminator).to(eq("Charlie"))
|
||||
expect(widget.paper_trail.originator).to(eq("Charlie"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "update! records timestamps" do
|
||||
wotsit = Wotsit.create!(name: "wotsit")
|
||||
wotsit.update!(name: "changed")
|
||||
reified = wotsit.versions.last.reify
|
||||
expect(reified.created_at).not_to(be_nil)
|
||||
expect(reified.updated_at).not_to(be_nil)
|
||||
end
|
||||
|
||||
it "update! does not raise error" do
|
||||
wotsit = Wotsit.create!(name: "name1")
|
||||
expect { wotsit.update!(name: "name2") }.not_to(raise_error)
|
||||
end
|
||||
|
||||
context "with a subclass" do
|
||||
let(:foo) { FooWidget.create }
|
||||
|
||||
before do
|
||||
foo.update!(name: "Foo")
|
||||
end
|
||||
|
||||
it "reify with the correct type" do
|
||||
expect(PaperTrail::Version.last.previous).to(eq(foo.versions.first))
|
||||
expect(PaperTrail::Version.last.next).to(be_nil)
|
||||
end
|
||||
|
||||
it "returns the correct originator" do
|
||||
PaperTrail.request.whodunnit = "Ben"
|
||||
foo.update_attribute(:name, "Geoffrey")
|
||||
expect(foo.paper_trail.originator).to(eq(PaperTrail.request.whodunnit))
|
||||
end
|
||||
|
||||
context "when destroyed" do
|
||||
before { foo.destroy }
|
||||
|
||||
it "reify with the correct type" do
|
||||
assert_kind_of(FooWidget, foo.versions.last.reify)
|
||||
expect(PaperTrail::Version.last.previous).to(eq(foo.versions[1]))
|
||||
expect(PaperTrail::Version.last.next).to(be_nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with an item with versions" do
|
||||
context "when the versions were created over time" do
|
||||
let(:widget) { Widget.create(name: "Widget") }
|
||||
let(:t0) { 2.days.ago }
|
||||
let(:t1) { 1.day.ago }
|
||||
let(:t2) { 1.hour.ago }
|
||||
|
||||
before do
|
||||
widget.update(name: "Fidget")
|
||||
widget.update(name: "Digit")
|
||||
widget.versions[0].update(created_at: t0)
|
||||
widget.versions[1].update(created_at: t1)
|
||||
widget.versions[2].update(created_at: t2)
|
||||
widget.update_attribute(:updated_at, t2)
|
||||
end
|
||||
|
||||
it "return nil for version_at before it was created" do
|
||||
expect(widget.paper_trail.version_at((t0 - 1))).to(be_nil)
|
||||
end
|
||||
|
||||
it "return how it looked when created for version_at its creation" do
|
||||
expect(widget.paper_trail.version_at(t0).name).to(eq("Widget"))
|
||||
end
|
||||
|
||||
it "return how it looked before its first update" do
|
||||
expect(widget.paper_trail.version_at((t1 - 1)).name).to(eq("Widget"))
|
||||
end
|
||||
|
||||
it "return how it looked after its first update" do
|
||||
expect(widget.paper_trail.version_at(t1).name).to(eq("Fidget"))
|
||||
end
|
||||
|
||||
it "return how it looked before its second update" do
|
||||
expect(widget.paper_trail.version_at((t2 - 1)).name).to(eq("Fidget"))
|
||||
end
|
||||
|
||||
it "return how it looked after its second update" do
|
||||
expect(widget.paper_trail.version_at(t2).name).to(eq("Digit"))
|
||||
end
|
||||
|
||||
it "return the current object for version_at after latest update" do
|
||||
expect(widget.paper_trail.version_at(1.day.from_now).name).to(eq("Digit"))
|
||||
end
|
||||
|
||||
it "still return a widget when appropriate, when passing timestamp as string" do
|
||||
expect(
|
||||
widget.paper_trail.version_at((t0 + 1.second).to_s).name
|
||||
).to(eq("Widget"))
|
||||
expect(
|
||||
widget.paper_trail.version_at((t1 + 1.second).to_s).name
|
||||
).to(eq("Fidget"))
|
||||
expect(
|
||||
widget.paper_trail.version_at((t2 + 1.second).to_s).name
|
||||
).to(eq("Digit"))
|
||||
end
|
||||
end
|
||||
|
||||
describe ".versions_between" do
|
||||
it "return versions in the time period" do
|
||||
widget = Widget.create(name: "Widget")
|
||||
widget.update(name: "Fidget")
|
||||
widget.update(name: "Digit")
|
||||
widget.versions[0].update(created_at: 30.days.ago)
|
||||
widget.versions[1].update(created_at: 15.days.ago)
|
||||
widget.versions[2].update(created_at: 1.day.ago)
|
||||
widget.update_attribute(:updated_at, 1.day.ago)
|
||||
expect(
|
||||
widget.paper_trail.versions_between(20.days.ago, 10.days.ago).map(&:name)
|
||||
).to(eq(["Fidget"]))
|
||||
expect(
|
||||
widget.paper_trail.versions_between(45.days.ago, 10.days.ago).map(&:name)
|
||||
).to(eq(%w[Widget Fidget]))
|
||||
expect(
|
||||
widget.paper_trail.versions_between(16.days.ago, 1.minute.ago).map(&:name)
|
||||
).to(eq(%w[Fidget Digit Digit]))
|
||||
expect(
|
||||
widget.paper_trail.versions_between(60.days.ago, 45.days.ago).map(&:name)
|
||||
).to(eq([]))
|
||||
end
|
||||
end
|
||||
|
||||
context "with the first version" do
|
||||
let(:widget) { Widget.create(name: "Widget") }
|
||||
let(:version) { widget.versions.last }
|
||||
|
||||
before do
|
||||
widget = Widget.create(name: "Widget")
|
||||
widget.update(name: "Fidget")
|
||||
widget.update(name: "Digit")
|
||||
end
|
||||
|
||||
it "have a nil previous version" do
|
||||
expect(version.previous).to(be_nil)
|
||||
end
|
||||
|
||||
it "return the next version" do
|
||||
expect(version.next).to(eq(widget.versions[1]))
|
||||
end
|
||||
|
||||
it "return the correct index" do
|
||||
expect(version.index).to(eq(0))
|
||||
end
|
||||
end
|
||||
|
||||
context "with the last version" do
|
||||
let(:widget) { Widget.create(name: "Widget") }
|
||||
let(:version) { widget.versions.last }
|
||||
|
||||
before do
|
||||
widget.update(name: "Fidget")
|
||||
widget.update(name: "Digit")
|
||||
end
|
||||
|
||||
it "return the previous version" do
|
||||
expect(version.previous).to(eq(widget.versions[(widget.versions.length - 2)]))
|
||||
end
|
||||
|
||||
it "have a nil next version" do
|
||||
expect(version.next).to(be_nil)
|
||||
end
|
||||
|
||||
it "return the correct index" do
|
||||
expect(version.index).to(eq((widget.versions.length - 1)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with an item" do
|
||||
let(:article) { Article.new(title: initial_title) }
|
||||
let(:initial_title) { "Foobar" }
|
||||
|
||||
context "when it is created" do
|
||||
before { article.save }
|
||||
|
||||
it "store fixed meta data" do
|
||||
expect(article.versions.last.answer).to(eq(42))
|
||||
end
|
||||
|
||||
it "store dynamic meta data which is independent of the item" do
|
||||
expect(article.versions.last.question).to(eq("31 + 11 = 42"))
|
||||
end
|
||||
|
||||
it "store dynamic meta data which depends on the item" do
|
||||
expect(article.versions.last.article_id).to(eq(article.id))
|
||||
end
|
||||
|
||||
it "store dynamic meta data based on a method of the item" do
|
||||
expect(article.versions.last.action).to(eq(article.action_data_provider_method))
|
||||
end
|
||||
|
||||
it "store dynamic meta data based on an attribute of the item at creation" do
|
||||
expect(article.versions.last.title).to(eq(initial_title))
|
||||
end
|
||||
end
|
||||
|
||||
context "when it is created, then updated" do
|
||||
before do
|
||||
article.save
|
||||
article.update!(content: "Better text.", title: "Rhubarb")
|
||||
end
|
||||
|
||||
it "store fixed meta data" do
|
||||
expect(article.versions.last.answer).to(eq(42))
|
||||
end
|
||||
|
||||
it "store dynamic meta data which is independent of the item" do
|
||||
expect(article.versions.last.question).to(eq("31 + 11 = 42"))
|
||||
end
|
||||
|
||||
it "store dynamic meta data which depends on the item" do
|
||||
expect(article.versions.last.article_id).to(eq(article.id))
|
||||
end
|
||||
|
||||
it "store dynamic meta data based on an attribute of the item prior to the update" do
|
||||
expect(article.versions.last.title).to(eq(initial_title))
|
||||
end
|
||||
end
|
||||
|
||||
context "when it is created, then destroyed" do
|
||||
before do
|
||||
article.save
|
||||
article.destroy
|
||||
end
|
||||
|
||||
it "store fixed metadata" do
|
||||
expect(article.versions.last.answer).to(eq(42))
|
||||
end
|
||||
|
||||
it "store dynamic metadata which is independent of the item" do
|
||||
expect(article.versions.last.question).to(eq("31 + 11 = 42"))
|
||||
end
|
||||
|
||||
it "store dynamic metadata which depends on the item" do
|
||||
expect(article.versions.last.article_id).to(eq(article.id))
|
||||
end
|
||||
|
||||
it "store dynamic metadata based on attribute of item prior to destruction" do
|
||||
expect(article.versions.last.title).to(eq(initial_title))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with a reified item" do
|
||||
it "know which version it came from, and return its previous self" do
|
||||
widget = Widget.create(name: "Bob")
|
||||
%w[Tom Dick Jane].each do |name|
|
||||
widget.update(name: name)
|
||||
end
|
||||
version = widget.versions.last
|
||||
widget = version.reify
|
||||
expect(widget.version).to(eq(version))
|
||||
expect(widget.paper_trail.previous_version).to(eq(widget.versions[-2].reify))
|
||||
end
|
||||
end
|
||||
|
||||
describe "#next_version" do
|
||||
context "with a reified item" do
|
||||
it "returns the object (not a Version) as it became next" do
|
||||
widget = Widget.create(name: "Bob")
|
||||
%w[Tom Dick Jane].each do |name|
|
||||
widget.update(name: name)
|
||||
end
|
||||
second_widget = widget.versions[1].reify
|
||||
last_widget = widget.versions.last.reify
|
||||
expect(second_widget.paper_trail.next_version.name).to(eq(widget.versions[2].reify.name))
|
||||
expect(widget.name).to(eq(last_widget.paper_trail.next_version.name))
|
||||
end
|
||||
end
|
||||
|
||||
context "with a non-reified item" do
|
||||
it "always returns nil because cannot ever have a next version" do
|
||||
widget = Widget.new
|
||||
expect(widget.paper_trail.next_version).to(be_nil)
|
||||
widget.save
|
||||
%w[Tom Dick Jane].each do |name|
|
||||
widget.update(name: name)
|
||||
end
|
||||
expect(widget.paper_trail.next_version).to(be_nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#previous_version" do
|
||||
context "with a reified item" do
|
||||
it "returns the object (not a Version) as it was most recently" do
|
||||
widget = Widget.create(name: "Bob")
|
||||
%w[Tom Dick Jane].each do |name|
|
||||
widget.update(name: name)
|
||||
end
|
||||
second_widget = widget.versions[1].reify
|
||||
last_widget = widget.versions.last.reify
|
||||
expect(second_widget.paper_trail.previous_version).to(be_nil)
|
||||
expect(last_widget.paper_trail.previous_version.name).to(eq(widget.versions[-2].reify.name))
|
||||
end
|
||||
end
|
||||
|
||||
context "with a non-reified item" do
|
||||
it "returns the object (not a Version) as it was most recently" do
|
||||
widget = Widget.new
|
||||
expect(widget.paper_trail.previous_version).to(be_nil)
|
||||
widget.save
|
||||
%w[Tom Dick Jane].each do |name|
|
||||
widget.update(name: name)
|
||||
end
|
||||
expect(widget.paper_trail.previous_version.name).to(eq(widget.versions.last.reify.name))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with :has_many :through" do
|
||||
it "store version on source <<" do
|
||||
book = Book.create(title: "War and Peace")
|
||||
dostoyevsky = Person.create(name: "Dostoyevsky")
|
||||
Person.create(name: "Solzhenitsyn")
|
||||
count = PaperTrail::Version.count
|
||||
(book.authors << dostoyevsky)
|
||||
expect((PaperTrail::Version.count - count)).to(eq(1))
|
||||
expect(book.authorships.first.versions.first).to(eq(PaperTrail::Version.last))
|
||||
end
|
||||
|
||||
it "store version on source create" do
|
||||
book = Book.create(title: "War and Peace")
|
||||
Person.create(name: "Dostoyevsky")
|
||||
Person.create(name: "Solzhenitsyn")
|
||||
count = PaperTrail::Version.count
|
||||
book.authors.create(name: "Tolstoy")
|
||||
expect((PaperTrail::Version.count - count)).to(eq(2))
|
||||
expect(
|
||||
[PaperTrail::Version.order(:id).to_a[-2].item, PaperTrail::Version.last.item]
|
||||
).to match_array([Person.last, Authorship.last])
|
||||
end
|
||||
|
||||
it "store version on join destroy" do
|
||||
book = Book.create(title: "War and Peace")
|
||||
dostoyevsky = Person.create(name: "Dostoyevsky")
|
||||
Person.create(name: "Solzhenitsyn")
|
||||
(book.authors << dostoyevsky)
|
||||
count = PaperTrail::Version.count
|
||||
book.authorships.reload.last.destroy
|
||||
expect((PaperTrail::Version.count - count)).to(eq(1))
|
||||
expect(PaperTrail::Version.last.reify.book).to(eq(book))
|
||||
expect(PaperTrail::Version.last.reify.author).to(eq(dostoyevsky))
|
||||
end
|
||||
|
||||
it "store version on join clear" do
|
||||
book = Book.create(title: "War and Peace")
|
||||
dostoyevsky = Person.create(name: "Dostoyevsky")
|
||||
Person.create(name: "Solzhenitsyn")
|
||||
book.authors << dostoyevsky
|
||||
count = PaperTrail::Version.count
|
||||
book.authorships.reload.destroy_all
|
||||
expect((PaperTrail::Version.count - count)).to(eq(1))
|
||||
expect(PaperTrail::Version.last.reify.book).to(eq(book))
|
||||
expect(PaperTrail::Version.last.reify.author).to(eq(dostoyevsky))
|
||||
end
|
||||
end
|
||||
|
||||
context "when the default accessor, length=, is overwritten" do
|
||||
it "returns overwritten value on reified instance" do
|
||||
song = Song.create(length: 4)
|
||||
song.update(length: 5)
|
||||
expect(song.length).to(eq(5))
|
||||
expect(song.versions.last.reify.length).to(eq(4))
|
||||
end
|
||||
end
|
||||
|
||||
context "when song name is a virtual attribute (no such db column)" do
|
||||
it "returns overwritten virtual attribute on the reified instance" do
|
||||
song = Song.create(length: 4)
|
||||
song.update(length: 5)
|
||||
song.name = "Good Vibrations"
|
||||
song.save
|
||||
song.name = "Yellow Submarine"
|
||||
expect(song.name).to(eq("Yellow Submarine"))
|
||||
expect(song.versions.last.reify.name).to(eq("Good Vibrations"))
|
||||
end
|
||||
end
|
||||
|
||||
context "with an unsaved record" do
|
||||
it "not have a version created on destroy" do
|
||||
widget = Widget.new
|
||||
widget.destroy
|
||||
expect(widget.versions.empty?).to(eq(true))
|
||||
end
|
||||
end
|
||||
|
||||
context "when measuring the memory allocation of" do
|
||||
let(:widget) do
|
||||
Widget.new(
|
||||
name: "Warble",
|
||||
a_text: "The quick brown fox",
|
||||
an_integer: 42,
|
||||
a_float: 153.01,
|
||||
a_decimal: 2.71828,
|
||||
a_boolean: true
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
# Json fields for `object` & `object_changes` attributes is most efficient way
|
||||
# to do the things - this way we will save even more RAM, as well as will skip
|
||||
# the whole YAML serialization
|
||||
allow(PaperTrail::Version).to receive(:object_changes_col_is_json?).and_return(true)
|
||||
allow(PaperTrail::Version).to receive(:object_col_is_json?).and_return(true)
|
||||
|
||||
# Force the loading of all lazy things like class definitions,
|
||||
# in order to get the pure benchmark
|
||||
version_building.call
|
||||
end
|
||||
|
||||
describe "#build_version_on_create" do
|
||||
let(:version_building) do
|
||||
lambda do
|
||||
widget.paper_trail.send(
|
||||
:build_version_on_create,
|
||||
in_after_callback: false
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it "is frugal enough" do
|
||||
# Some time ago there was 95kbs..
|
||||
# At the time of commit the test passes with assertion on 17kbs.
|
||||
# Lets assert 20kbs then, to avoid flaky fails.
|
||||
expect(&version_building).to allocate_less_than(20).kilobytes
|
||||
end
|
||||
end
|
||||
|
||||
describe "#build_version_on_update" do
|
||||
let(:widget) do
|
||||
super().tap do |w|
|
||||
w.save!
|
||||
w.attributes = {
|
||||
name: "Dostoyevsky",
|
||||
a_text: "The slow yellow mouse",
|
||||
an_integer: 84,
|
||||
a_float: 306.02,
|
||||
a_decimal: 5.43656,
|
||||
a_boolean: false
|
||||
}
|
||||
end
|
||||
end
|
||||
let(:version_building) do
|
||||
lambda do
|
||||
widget.paper_trail.send(
|
||||
:build_version_on_update,
|
||||
force: false,
|
||||
in_after_callback: false,
|
||||
is_touch: false
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it "is frugal enough" do
|
||||
# Some time ago there was 144kbs..
|
||||
# At the time of commit the test passes with assertion on 27kbs.
|
||||
# Lets assert 35kbs then, to avoid flaky fails.
|
||||
expect(&version_building).to allocate_less_than(35).kilobytes
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue