spec/integration/integration_spec.rb in mongoid-history-0.4.0 vs spec/integration/integration_spec.rb in mongoid-history-0.4.1
- old
+ new
@@ -1,754 +1,788 @@
-require 'spec_helper'
-
-describe Mongoid::History do
- before :all do
- class Post
- include Mongoid::Document
- include Mongoid::Timestamps
- include Mongoid::History::Trackable
-
- field :title
- field :body
- field :rating
-
- embeds_many :comments, store_as: :coms
- embeds_one :section, store_as: :sec
- embeds_many :tags, :cascade_callbacks => true
-
- accepts_nested_attributes_for :tags, :allow_destroy => true
-
- track_history :on => [:title, :body], :track_destroy => true
- end
-
- class Comment
- include Mongoid::Document
- include Mongoid::Timestamps
- include Mongoid::History::Trackable
-
- field :t, as: :title
- field :body
- embedded_in :commentable, polymorphic: true
- track_history :on => [:title, :body], :scope => :post, :track_create => true, :track_destroy => true
- end
-
- class Section
- include Mongoid::Document
- include Mongoid::Timestamps
- include Mongoid::History::Trackable
-
- field :t, as: :title
- embedded_in :post
- track_history :on => [:title], :scope => :post, :track_create => true, :track_destroy => true
- end
-
- class User
- include Mongoid::Document
- include Mongoid::Timestamps
- include Mongoid::History::Trackable
-
- field :n, as: :name
- field :em, as: :email
- field :phone
- field :address
- field :city
- field :country
- field :aliases, :type => Array
- track_history :except => [:email, :updated_at]
- end
-
- class Tag
- include Mongoid::Document
- # include Mongoid::Timestamps (see: https://github.com/mongoid/mongoid/issues/3078)
- include Mongoid::History::Trackable
-
- belongs_to :updated_by, :class_name => "User"
-
- field :title
- track_history :on => [:title], :scope => :post, :track_create => true, :track_destroy => true, :modifier_field => :updated_by
- end
-
- class Foo < Comment
- end
-
- @persisted_history_options = Mongoid::History.trackable_class_options
- end
-
- before(:each){ Mongoid::History.trackable_class_options = @persisted_history_options }
- let(:user){ User.create(name: "Aaron", email: "aaron@randomemail.com", aliases: ['bob'], country: 'Canada', city: 'Toronto', address: '21 Jump Street') }
- let(:another_user){ User.create(:name => "Another Guy", :email => "anotherguy@randomemail.com") }
- let(:post){ Post.create(:title => "Test", :body => "Post", :modifier => user, :views => 100) }
- let(:comment){ post.comments.create(:title => "test", :body => "comment", :modifier => user) }
- let(:tag){ Tag.create(:title => "test") }
-
- describe "track" do
- describe "on creation" do
- it "should have one history track in comment" do
- comment.history_tracks.count.should == 1
- end
-
- it "should assign title and body on modified" do
- comment.history_tracks.first.modified.should == {'t' => "test", 'body' => "comment"}
- end
-
- it "should not assign title and body on original" do
- comment.history_tracks.first.original.should == {}
- end
-
- it "should assign modifier" do
- comment.history_tracks.first.modifier.should == user
- end
-
- it "should assign version" do
- comment.history_tracks.first.version.should == 1
- end
-
- it "should assign scope" do
- comment.history_tracks.first.scope.should == "post"
- end
-
- it "should assign method" do
- comment.history_tracks.first.action.should == "create"
- end
-
- it "should assign association_chain" do
- expected = [
- {'id' => post.id, 'name' => "Post"},
- {'id' => comment.id, 'name' => "coms"}
- ]
- comment.history_tracks.first.association_chain.should == expected
- end
- end
-
- describe "on destruction" do
- it "should have two history track records in post" do
- lambda {
- post.destroy
- }.should change(Tracker, :count).by(1)
- end
-
- it "should assign destroy on track record" do
- post.destroy
- post.history_tracks.last.action.should == "destroy"
- end
-
- it "should return affected attributes from track record" do
- post.destroy
- post.history_tracks.last.affected["title"].should == "Test"
- end
- end
-
- describe "on update non-embedded" do
- it "should create a history track if changed attributes match tracked attributes" do
- lambda {
- post.update_attributes(:title => "Another Test")
- }.should change(Tracker, :count).by(1)
- end
-
- it "should not create a history track if changed attributes do not match tracked attributes" do
- lambda {
- post.update_attributes(:rating => "untracked")
- }.should change(Tracker, :count).by(0)
- end
-
- it "should assign modified fields" do
- post.update_attributes(:title => "Another Test")
- post.history_tracks.last.modified.should == {
- "title" => "Another Test"
- }
- end
-
- it "should assign method field" do
- post.update_attributes(:title => "Another Test")
- post.history_tracks.last.action.should == "update"
- end
-
- it "should assign original fields" do
- post.update_attributes(:title => "Another Test")
- post.history_tracks.last.original.should == {
- "title" => "Test"
- }
- end
-
- it "should assign modifier" do
- post.update_attributes(:title => "Another Test")
- post.history_tracks.first.modifier.should == user
- end
-
- it "should assign version on history tracks" do
- post.update_attributes(:title => "Another Test")
- post.history_tracks.first.version.should == 1
- end
-
- it "should assign version on post" do
- post.update_attributes(:title => "Another Test")
- post.version.should == 1
- end
-
- it "should assign scope" do
- post.update_attributes(:title => "Another Test")
- post.history_tracks.first.scope.should == "post"
- end
-
- it "should assign association_chain" do
- post.update_attributes(:title => "Another Test")
- post.history_tracks.last.association_chain.should == [{'id' => post.id, 'name' => "Post"}]
- end
-
- it "should exclude defined options" do
- name = user.name
- user.update_attributes(:name => "Aaron2", :email => "aaronsnewemail@randomemail.com")
- user.history_tracks.first.original.keys.should == [ "n" ]
- user.history_tracks.first.original["n"].should == name
- user.history_tracks.first.modified.keys.should == [ "n" ]
- user.history_tracks.first.modified["n"].should == user.name
- end
-
- it "should undo field changes" do
- name = user.name
- user.update_attributes(:name => "Aaron2", :email => "aaronsnewemail@randomemail.com")
- user.history_tracks.first.undo! nil
- user.reload.name.should == name
- end
-
- it "should undo non-existing field changes" do
- post = Post.create(:modifier => user, :views => 100)
- post.reload.title.should == nil
- post.update_attributes(:title => "Aaron2")
- post.reload.title.should == "Aaron2"
- post.history_tracks.first.undo! nil
- post.reload.title.should == nil
- end
-
- it "should track array changes" do
- aliases = user.aliases
- user.update_attributes(:aliases => [ 'bob', 'joe' ])
- user.history_tracks.first.original["aliases"].should == aliases
- user.history_tracks.first.modified["aliases"].should == user.aliases
- end
-
- it "should undo array changes" do
- aliases = user.aliases
- user.update_attributes(:aliases => [ 'bob', 'joe' ])
- user.history_tracks.first.undo! nil
- user.reload.aliases.should == aliases
- end
- end
-
- describe "#tracked_changes" do
- context "create action" do
- subject{ tag.history_tracks.first.tracked_changes }
- it "consider all fields values as :to" do
- subject[:title].should == {to: "test"}.with_indifferent_access
- end
- end
- context "destroy action" do
- subject{ tag.destroy; tag.history_tracks.last.tracked_changes }
- it "consider all fields values as :from" do
- subject[:title].should == {from: "test"}.with_indifferent_access
- end
- end
- context "update action" do
- subject{ user.history_tracks.first.tracked_changes }
- before do
- user.update_attributes(name: "Aaron2", email: nil, country: '', city: nil, phone: '867-5309', aliases: ['','bill','james'])
- end
- it{ should be_a HashWithIndifferentAccess }
- it "should track changed field" do
- subject[:n].should == {from: "Aaron", to:"Aaron2"}.with_indifferent_access
- end
- it "should track added field" do
- subject[:phone].should == {to: "867-5309"}.with_indifferent_access
- end
- it "should track removed field" do
- subject[:city].should == {from: "Toronto"}.with_indifferent_access
- end
- it "should not consider blank as removed" do
- subject[:country].should == {from: "Canada", to: ''}.with_indifferent_access
- end
- it "should track changed array field" do
- subject[:aliases].should == {from: ["bob"], to: ["", "bill", "james"]}.with_indifferent_access
- end
- it "should not track unmodified field" do
- subject[:address].should be_nil
- end
- it "should not track untracked fields" do
- subject[:email].should be_nil
- end
- end
- end
-
- describe "#tracked_edits" do
- context "create action" do
- subject{ tag.history_tracks.first.tracked_edits }
- it "consider all edits as ;add" do
- subject[:add].should == {title: "test"}.with_indifferent_access
- end
- end
- context "destroy action" do
- subject{ tag.destroy; tag.history_tracks.last.tracked_edits }
- it "consider all edits as ;remove" do
- subject[:remove].should == {title: "test"}.with_indifferent_access
- end
- end
- context "update action" do
- subject{ user.history_tracks.first.tracked_edits }
- before do
- user.update_attributes(name: "Aaron2", email: nil, country: '', city: nil, phone: '867-5309', aliases: ['','bill','james'])
- end
- it{ should be_a HashWithIndifferentAccess }
- it "should track changed field" do
- subject[:modify].should == {n: {from: "Aaron", to:"Aaron2"}}.with_indifferent_access
- end
- it "should track added field" do
- subject[:add].should == {phone: "867-5309"}.with_indifferent_access
- end
- it "should track removed field and consider blank as removed" do
- subject[:remove].should == {city: "Toronto", country: "Canada"}.with_indifferent_access
- end
- it "should track changed array field" do
- subject[:array].should == {aliases: {remove: ["bob"], add: ["", "bill", "james"]}}.with_indifferent_access
- end
- it "should not track unmodified field" do
- %w(add modify remove array).each do |edit|
- subject[edit][:address].should be_nil
- end
- end
- it "should not track untracked fields" do
- %w(add modify remove array).each do |edit|
- subject[edit][:email].should be_nil
- end
- end
- end
- context "with empty values" do
- subject{ Tracker.new }
- it "should skip empty values" do
- subject.stub(:tracked_changes){ {name:{to:'',from:[]}, city:{to:'Toronto',from:''}} }
- subject.tracked_edits.should == {add: {city: "Toronto"}}.with_indifferent_access
- end
- end
- end
-
- describe "on update non-embedded twice" do
- it "should assign version on post" do
- post.update_attributes(:title => "Test2")
- post.update_attributes(:title => "Test3")
- post.version.should == 2
- end
-
- it "should create a history track if changed attributes match tracked attributes" do
- lambda {
- post.update_attributes(:title => "Test2")
- post.update_attributes(:title => "Test3")
- }.should change(Tracker, :count).by(2)
- end
-
- it "should create a history track of version 2" do
- post.update_attributes(:title => "Test2")
- post.update_attributes(:title => "Test3")
- post.history_tracks.where(:version => 2).first.should_not be_nil
- end
-
- it "should assign modified fields" do
- post.update_attributes(:title => "Test2")
- post.update_attributes(:title => "Test3")
- post.history_tracks.where(:version => 2).first.modified.should == {
- "title" => "Test3"
- }
- end
-
- it "should assign original fields" do
- post.update_attributes(:title => "Test2")
- post.update_attributes(:title => "Test3")
- post.history_tracks.where(:version => 2).first.original.should == {
- "title" => "Test2"
- }
- end
-
-
- it "should assign modifier" do
- post.update_attributes(:title => "Another Test", :modifier => another_user)
- post.history_tracks.last.modifier.should == another_user
- end
- end
-
- describe "on update embedded 1..N (embeds_many)" do
- it "should assign version on comment" do
- comment.update_attributes(:title => "Test2")
- comment.version.should == 2 # first track generated on creation
- end
-
- it "should create a history track of version 2" do
- comment.update_attributes(:title => "Test2")
- comment.history_tracks.where(:version => 2).first.should_not be_nil
- end
-
- it "should assign modified fields" do
- comment.update_attributes(:t => "Test2")
- comment.history_tracks.where(:version => 2).first.modified.should == {
- "t" => "Test2"
- }
- end
-
- it "should assign original fields" do
- comment.update_attributes(:title => "Test2")
- comment.history_tracks.where(:version => 2).first.original.should == {
- "t" => "test"
- }
- end
-
- it "should be possible to undo from parent" do
- comment.update_attributes(:title => "Test 2")
- user
- post.history_tracks.last.undo!(user)
- comment.reload
- comment.title.should == "test"
- end
-
- it "should assign modifier" do
- post.update_attributes(:title => "Another Test", :modifier => another_user)
- post.history_tracks.last.modifier.should == another_user
- end
- end
-
- describe "on update embedded 1..1 (embeds_one)" do
- let(:section){ Section.new(:title => 'Technology') }
-
- before(:each) do
- post.section = section
- post.save!
- post.reload
- section = post.section
- end
-
- it "should assign version on create section" do
- section.version.should == 1
- end
-
- it "should assign version on section" do
- section.update_attributes(:title => 'Technology 2')
- section.version.should == 2 # first track generated on creation
- end
-
- it "should create a history track of version 2" do
- section.update_attributes(:title => 'Technology 2')
- section.history_tracks.where(:version => 2).first.should_not be_nil
- end
-
- it "should assign modified fields" do
- section.update_attributes(:title => 'Technology 2')
- section.history_tracks.where(:version => 2).first.modified.should == {
- "t" => "Technology 2"
- }
- end
-
- it "should assign original fields" do
- section.update_attributes(:title => 'Technology 2')
- section.history_tracks.where(:version => 2).first.original.should == {
- "t" => "Technology"
- }
- end
-
- it "should be possible to undo from parent" do
- section.update_attributes(:title => 'Technology 2')
- post.history_tracks.last.undo!(user)
- section.reload
- section.title.should == "Technology"
- end
-
- it "should assign modifier" do
- section.update_attributes(:title => "Business", :modifier => another_user)
- post.history_tracks.last.modifier.should == another_user
- end
- end
-
- describe "on destroy embedded" do
- it "should be possible to re-create destroyed embedded" do
- comment.destroy
- comment.history_tracks.last.undo!(user)
- post.reload
- post.comments.first.title.should == "test"
- end
-
- it "should be possible to re-create destroyed embedded from parent" do
- comment.destroy
- post.history_tracks.last.undo!(user)
- post.reload
- post.comments.first.title.should == "test"
- end
-
- it "should be possible to destroy after re-create embedded from parent" do
- comment.destroy
- post.history_tracks.last.undo!(user)
- post.history_tracks.last.undo!(user)
- post.reload
- post.comments.count.should == 0
- end
-
- it "should be possible to create with redo after undo create embedded from parent" do
- comment # initialize
- post.comments.create!(:title => "The second one")
- track = post.history_tracks.last
- track.undo!(user)
- track.redo!(user)
- post.reload
- post.comments.count.should == 2
- end
- end
-
- describe "embedded with cascading callbacks" do
-
- let(:tag_foo){ post.tags.create(:title => "foo", :updated_by => user) }
- let(:tag_bar){ post.tags.create(:title => "bar") }
-
- before(:each) do
- Mongoid.instantiate_observers
- Thread.current[:mongoid_history_sweeper_controller] = Mongoid::History::Sweeper.instance
- Mongoid::History::Sweeper.instance.stub(:current_user){ user }
- end
-
- # it "should have cascaded the creation callbacks and set timestamps" do
- # tag_foo; tag_bar # initialize
- # tag_foo.created_at.should_not be_nil
- # tag_foo.updated_at.should_not be_nil
- # end
-
- it "should allow an update through the parent model" do
- update_hash = { "post" => { "tags_attributes" => { "1234" => { "id" => tag_bar.id, "title" => "baz" } } } }
- post.update_attributes(update_hash["post"])
- post.tags.last.title.should == "baz"
- end
-
- it "should be possible to destroy through parent model using canoncial _destroy macro" do
- tag_foo; tag_bar # initialize
- post.tags.count.should == 2
- update_hash = { "post" => { "tags_attributes" => { "1234" => { "id" => tag_bar.id, "title" => "baz", "_destroy" => "true"} } } }
- post.update_attributes(update_hash["post"])
- post.tags.count.should == 1
- post.history_tracks.last.action.should == "destroy"
- end
-
- it "should write relationship name for association_chain hiearchy instead of class name when using _destroy macro" do
- update_hash = {"tags_attributes" => { "1234" => { "id" => tag_foo.id, "_destroy" => "1"} } }
- post.update_attributes(update_hash)
-
- # historically this would have evaluated to 'Tags' and an error would be thrown
- # on any call that walked up the association_chain, e.g. 'trackable'
- tag_foo.history_tracks.last.association_chain.last["name"].should == "tags"
- lambda{ tag_foo.history_tracks.last.trackable }.should_not raise_error
- end
-
- it "should save modifier" do
- Thread.current[:mongoid_history_sweeper_controller].current_user.should eq user
- tag_foo.history_tracks.last.modifier.should eq user
- tag_bar.history_tracks.last.modifier.should eq user
- end
- end
-
- describe "non-embedded" do
- it "should undo changes" do
- post.update_attributes(:title => "Test2")
- post.history_tracks.where(:version => 1).last.undo!(user)
- post.reload
- post.title.should == "Test"
- end
-
- it "should undo destruction" do
- post.destroy
- post.history_tracks.where(:version => 1).last.undo!(user)
- Post.find(post.id).title.should == "Test"
- end
-
- it "should create a new history track after undo" do
- comment # initialize
- post.update_attributes(:title => "Test2")
- post.history_tracks.last.undo!(user)
- post.reload
- post.history_tracks.count.should == 3
- end
-
- it "should assign user as the modifier of the newly created history track" do
- post.update_attributes(:title => "Test2")
- post.history_tracks.where(:version => 1).last.undo!(user)
- post.reload
- post.history_tracks.where(:version => 2).last.modifier.should == user
- end
-
- it "should stay the same after undo and redo" do
- post.update_attributes(:title => "Test2")
- track = post.history_tracks.last
- track.undo!(user)
- track.redo!(user)
- post2 = Post.where(:_id => post.id).first
-
- post.title.should == post2.title
- end
-
- it "should be destroyed after undo and redo" do
- post.destroy
- track = post.history_tracks.where(:version => 1).last
- track.undo!(user)
- track.redo!(user)
- Post.where(:_id => post.id).first.should == nil
- end
- end
-
- describe "embedded" do
- it "should undo changes" do
- comment.update_attributes(:title => "Test2")
- comment.history_tracks.where(:version => 2).first.undo!(user)
- comment.reload
- comment.title.should == "test"
- end
-
- it "should create a new history track after undo" do
- comment.update_attributes(:title => "Test2")
- comment.history_tracks.where(:version => 2).first.undo!(user)
- comment.reload
- comment.history_tracks.count.should == 3
- end
-
- it "should assign user as the modifier of the newly created history track" do
- comment.update_attributes(:title => "Test2")
- comment.history_tracks.where(:version => 2).first.undo!(user)
- comment.reload
- comment.history_tracks.where(:version => 3).first.modifier.should == user
- end
-
- it "should stay the same after undo and redo" do
- comment.update_attributes(:title => "Test2")
- track = comment.history_tracks.where(:version => 2).first
- track.undo!(user)
- track.redo!(user)
- comment.reload
- comment.title.should == "Test2"
- end
- end
-
- describe "trackables" do
- before :each do
- comment.update_attributes(:title => "Test2") # version == 2
- comment.update_attributes(:title => "Test3") # version == 3
- comment.update_attributes(:title => "Test4") # version == 4
- end
-
- describe "undo" do
- it "should recognize :from, :to options" do
- comment.undo! user, :from => 4, :to => 2
- comment.title.should == "test"
- end
-
- it "should recognize parameter as version number" do
- comment.undo! user, 3
- comment.title.should == "Test2"
- end
-
- it "should undo last version when no parameter is specified" do
- comment.undo! user
- comment.title.should == "Test3"
- end
-
- it "should recognize :last options" do
- comment.undo! user, :last => 2
- comment.title.should == "Test2"
- end
-
- end
-
- describe "redo" do
- before :each do
- comment.update_attributes(:title => "Test5")
- end
-
- it "should recognize :from, :to options" do
- comment.redo! user, :from => 2, :to => 4
- comment.title.should == "Test4"
- end
-
- it "should recognize parameter as version number" do
- comment.redo! user, 2
- comment.title.should == "Test2"
- end
-
- it "should redo last version when no parameter is specified" do
- comment.redo! user
- comment.title.should == "Test5"
- end
-
- it "should recognize :last options" do
- comment.redo! user, :last => 1
- comment.title.should == "Test5"
- end
-
- end
- end
-
- describe "localized fields" do
- before :each do
- class Sausage
- include Mongoid::Document
- include Mongoid::History::Trackable
-
- field :flavour, localize: true
- track_history :on => [:flavour], :track_destroy => true
- end
- end
- it "should correctly undo and redo" do
- if Sausage.respond_to?(:localized_fields)
- sausage = Sausage.create(flavour_translations: { 'en' => "Apple", 'nl' => 'Appel' } )
- sausage.update_attributes(:flavour => "Guinness")
-
- track = sausage.history_tracks.last
-
- track.undo! user
- sausage.reload.flavour.should == "Apple"
-
- track.redo! user
- sausage.reload.flavour.should == "Guinness"
-
- sausage.destroy
- sausage.history_tracks.last.action.should == "destroy"
- sausage.history_tracks.last.undo! user
- sausage.reload.flavour.should == "Guinness"
- end
- end
- end
-
- describe "embedded with a polymorphic trackable" do
- let(:foo){ Foo.new(:title => 'a title', :body => 'a body') }
- before :each do
- post.comments << foo
- post.save
- end
- it "should assign interface name in association chain" do
- foo.update_attribute(:body, 'a changed body')
- expected_root = {"name" => "Post", "id" => post.id}
- expected_node = {"name" => "coms", "id" => foo.id}
- foo.history_tracks.first.association_chain.should == [expected_root, expected_node]
- end
- end
-
- describe "#trackable_parent_class" do
- context "a non-embedded model" do
- it "should return the trackable parent class" do
- tag.history_tracks.first.trackable_parent_class.should == Tag
- end
- it "should return the parent class even if the trackable is deleted" do
- tracker = tag.history_tracks.first
- tag.destroy
- tracker.trackable_parent_class.should == Tag
- end
- end
- context "an embedded model" do
- it "should return the trackable parent class" do
- comment.update_attributes(title: "Foo")
- comment.history_tracks.first.trackable_parent_class.should == Post
- end
- it "should return the parent class even if the trackable is deleted" do
- tracker = comment.history_tracks.first
- comment.destroy
- tracker.trackable_parent_class.should == Post
- end
- end
- end
- end
-end
+require 'spec_helper'
+
+describe Mongoid::History do
+ before :all do
+ class Post
+ include Mongoid::Document
+ include Mongoid::Timestamps
+ include Mongoid::History::Trackable
+
+ field :title
+ field :body
+ field :rating
+ field :views, type: Integer
+
+ embeds_many :comments, store_as: :coms
+ embeds_one :section, store_as: :sec
+ embeds_many :tags, cascade_callbacks: true
+
+ accepts_nested_attributes_for :tags, allow_destroy: true
+
+ track_history on: [:title, :body], track_destroy: true
+ end
+
+ class Comment
+ include Mongoid::Document
+ include Mongoid::Timestamps
+ include Mongoid::History::Trackable
+
+ field :t, as: :title
+ field :body
+ embedded_in :commentable, polymorphic: true
+ track_history on: [:title, :body], scope: :post, track_create: true, track_destroy: true
+ end
+
+ class Section
+ include Mongoid::Document
+ include Mongoid::Timestamps
+ include Mongoid::History::Trackable
+
+ field :t, as: :title
+ embedded_in :post
+ track_history on: [:title], scope: :post, track_create: true, track_destroy: true
+ end
+
+ class User
+ include Mongoid::Document
+ include Mongoid::Timestamps
+ include Mongoid::History::Trackable
+
+ field :n, as: :name
+ field :em, as: :email
+ field :phone
+ field :address
+ field :city
+ field :country
+ field :aliases, type: Array
+ track_history except: [:email, :updated_at]
+ end
+
+ class Tag
+ include Mongoid::Document
+ # include Mongoid::Timestamps (see: https://github.com/mongoid/mongoid/issues/3078)
+ include Mongoid::History::Trackable
+
+ belongs_to :updated_by, class_name: "User"
+
+ field :title
+ track_history on: [:title], scope: :post, track_create: true, track_destroy: true, modifier_field: :updated_by
+ end
+
+ class Foo < Comment
+ end
+
+ @persisted_history_options = Mongoid::History.trackable_class_options
+ end
+
+ before(:each) { Mongoid::History.trackable_class_options = @persisted_history_options }
+ let(:user) { User.create(name: "Aaron", email: "aaron@randomemail.com", aliases: ['bob'], country: 'Canada', city: 'Toronto', address: '21 Jump Street') }
+ let(:another_user) { User.create(name: "Another Guy", email: "anotherguy@randomemail.com") }
+ let(:post) { Post.create(title: "Test", body: "Post", modifier: user, views: 100) }
+ let(:comment) { post.comments.create(title: "test", body: "comment", modifier: user) }
+ let(:tag) { Tag.create(title: "test") }
+
+ describe "track" do
+ describe "on creation" do
+ it "should have one history track in comment" do
+ comment.history_tracks.count.should == 1
+ end
+
+ it "should assign title and body on modified" do
+ comment.history_tracks.first.modified.should == { 't' => "test", 'body' => "comment" }
+ end
+
+ it "should not assign title and body on original" do
+ comment.history_tracks.first.original.should == {}
+ end
+
+ it "should assign modifier" do
+ comment.history_tracks.first.modifier.should == user
+ end
+
+ it "should assign version" do
+ comment.history_tracks.first.version.should == 1
+ end
+
+ it "should assign scope" do
+ comment.history_tracks.first.scope.should == "post"
+ end
+
+ it "should assign method" do
+ comment.history_tracks.first.action.should == "create"
+ end
+
+ it "should assign association_chain" do
+ expected = [
+ { 'id' => post.id, 'name' => "Post" },
+ { 'id' => comment.id, 'name' => "coms" }
+ ]
+ comment.history_tracks.first.association_chain.should == expected
+ end
+ end
+
+ describe "on destruction" do
+ it "should have two history track records in post" do
+ lambda {
+ post.destroy
+ }.should change(Tracker, :count).by(1)
+ end
+
+ it "should assign destroy on track record" do
+ post.destroy
+ post.history_tracks.last.action.should == "destroy"
+ end
+
+ it "should return affected attributes from track record" do
+ post.destroy
+ post.history_tracks.last.affected["title"].should == "Test"
+ end
+ end
+
+ describe "on update non-embedded" do
+ it "should create a history track if changed attributes match tracked attributes" do
+ lambda {
+ post.update_attributes(title: "Another Test")
+ }.should change(Tracker, :count).by(1)
+ end
+
+ it "should not create a history track if changed attributes do not match tracked attributes" do
+ lambda {
+ post.update_attributes(rating: "untracked")
+ }.should change(Tracker, :count).by(0)
+ end
+
+ it "should assign modified fields" do
+ post.update_attributes(title: "Another Test")
+ post.history_tracks.last.modified.should == {
+ "title" => "Another Test"
+ }
+ end
+
+ it "should assign method field" do
+ post.update_attributes(title: "Another Test")
+ post.history_tracks.last.action.should == "update"
+ end
+
+ it "should assign original fields" do
+ post.update_attributes(title: "Another Test")
+ post.history_tracks.last.original.should == {
+ "title" => "Test"
+ }
+ end
+
+ it "should assign modifier" do
+ post.update_attributes(title: "Another Test")
+ post.history_tracks.first.modifier.should == user
+ end
+
+ it "should assign version on history tracks" do
+ post.update_attributes(title: "Another Test")
+ post.history_tracks.first.version.should == 1
+ end
+
+ it "should assign version on post" do
+ post.update_attributes(title: "Another Test")
+ post.version.should == 1
+ end
+
+ it "should assign scope" do
+ post.update_attributes(title: "Another Test")
+ post.history_tracks.first.scope.should == "post"
+ end
+
+ it "should assign association_chain" do
+ post.update_attributes(title: "Another Test")
+ post.history_tracks.last.association_chain.should == [{ 'id' => post.id, 'name' => "Post" }]
+ end
+
+ it "should exclude defined options" do
+ name = user.name
+ user.update_attributes(name: "Aaron2", email: "aaronsnewemail@randomemail.com")
+ user.history_tracks.first.original.keys.should == ["n"]
+ user.history_tracks.first.original["n"].should == name
+ user.history_tracks.first.modified.keys.should == ["n"]
+ user.history_tracks.first.modified["n"].should == user.name
+ end
+
+ it "should undo field changes" do
+ name = user.name
+ user.update_attributes(name: "Aaron2", email: "aaronsnewemail@randomemail.com")
+ user.history_tracks.first.undo! nil
+ user.reload.name.should == name
+ end
+
+ it "should undo non-existing field changes" do
+ post = Post.create(modifier: user, views: 100)
+ post.reload.title.should be_nil
+ post.update_attributes(title: "Aaron2")
+ post.reload.title.should == "Aaron2"
+ post.history_tracks.first.undo! nil
+ post.reload.title.should be_nil
+ end
+
+ it "should track array changes" do
+ aliases = user.aliases
+ user.update_attributes(aliases: ['bob', 'joe'])
+ user.history_tracks.first.original["aliases"].should == aliases
+ user.history_tracks.first.modified["aliases"].should == user.aliases
+ end
+
+ it "should undo array changes" do
+ aliases = user.aliases
+ user.update_attributes(aliases: ['bob', 'joe'])
+ user.history_tracks.first.undo! nil
+ user.reload.aliases.should == aliases
+ end
+ end
+
+ describe "#tracked_changes" do
+ context "create action" do
+ subject { tag.history_tracks.first.tracked_changes }
+ it "consider all fields values as :to" do
+ subject[:title].should == { to: "test" }.with_indifferent_access
+ end
+ end
+ context "destroy action" do
+ subject {
+ tag.destroy
+ tag.history_tracks.last.tracked_changes
+ }
+ it "consider all fields values as :from" do
+ subject[:title].should == { from: "test" }.with_indifferent_access
+ end
+ end
+ context "update action" do
+ subject { user.history_tracks.first.tracked_changes }
+ before do
+ user.update_attributes(name: "Aaron2", email: nil, country: '', city: nil, phone: '867-5309', aliases: ['', 'bill', 'james'])
+ end
+ it { should be_a HashWithIndifferentAccess }
+ it "should track changed field" do
+ subject[:n].should == { from: "Aaron", to: "Aaron2" }.with_indifferent_access
+ end
+ it "should track added field" do
+ subject[:phone].should == { to: "867-5309" }.with_indifferent_access
+ end
+ it "should track removed field" do
+ subject[:city].should == { from: "Toronto" }.with_indifferent_access
+ end
+ it "should not consider blank as removed" do
+ subject[:country].should == { from: "Canada", to: '' }.with_indifferent_access
+ end
+ it "should track changed array field" do
+ subject[:aliases].should == { from: ["bob"], to: ["", "bill", "james"] }.with_indifferent_access
+ end
+ it "should not track unmodified field" do
+ subject[:address].should be_nil
+ end
+ it "should not track untracked fields" do
+ subject[:email].should be_nil
+ end
+ end
+ end
+
+ describe "#tracked_edits" do
+ context "create action" do
+ subject { tag.history_tracks.first.tracked_edits }
+ it "consider all edits as ;add" do
+ subject[:add].should == { title: "test" }.with_indifferent_access
+ end
+ end
+ context "destroy action" do
+ subject {
+ tag.destroy
+ tag.history_tracks.last.tracked_edits
+ }
+ it "consider all edits as ;remove" do
+ subject[:remove].should == { title: "test" }.with_indifferent_access
+ end
+ end
+ context "update action" do
+ subject { user.history_tracks.first.tracked_edits }
+ before do
+ user.update_attributes(name: "Aaron2", email: nil, country: '', city: nil, phone: '867-5309', aliases: ['', 'bill', 'james'])
+ end
+ it { should be_a HashWithIndifferentAccess }
+ it "should track changed field" do
+ subject[:modify].should == { n: { from: "Aaron", to: "Aaron2" } }.with_indifferent_access
+ end
+ it "should track added field" do
+ subject[:add].should == { phone: "867-5309" }.with_indifferent_access
+ end
+ it "should track removed field and consider blank as removed" do
+ subject[:remove].should == { city: "Toronto", country: "Canada" }.with_indifferent_access
+ end
+ it "should track changed array field" do
+ subject[:array].should == { aliases: { remove: ["bob"], add: ["", "bill", "james"] } }.with_indifferent_access
+ end
+ it "should not track unmodified field" do
+ %w(add modify remove array).each do |edit|
+ subject[edit][:address].should be_nil
+ end
+ end
+ it "should not track untracked fields" do
+ %w(add modify remove array).each do |edit|
+ subject[edit][:email].should be_nil
+ end
+ end
+ end
+ context "with empty values" do
+ subject { Tracker.new }
+ it "should skip empty values" do
+ subject.stub(:tracked_changes) { { name: { to: '', from: [] }, city: { to: 'Toronto', from: '' } } }
+ subject.tracked_edits.should == { add: { city: "Toronto" } }.with_indifferent_access
+ end
+ end
+ end
+
+ describe "on update non-embedded twice" do
+ it "should assign version on post" do
+ post.update_attributes(title: "Test2")
+ post.update_attributes(title: "Test3")
+ post.version.should == 2
+ end
+
+ it "should create a history track if changed attributes match tracked attributes" do
+ lambda {
+ post.update_attributes(title: "Test2")
+ post.update_attributes(title: "Test3")
+ }.should change(Tracker, :count).by(2)
+ end
+
+ it "should create a history track of version 2" do
+ post.update_attributes(title: "Test2")
+ post.update_attributes(title: "Test3")
+ post.history_tracks.where(version: 2).first.should_not be_nil
+ end
+
+ it "should assign modified fields" do
+ post.update_attributes(title: "Test2")
+ post.update_attributes(title: "Test3")
+ post.history_tracks.where(version: 2).first.modified.should == {
+ "title" => "Test3"
+ }
+ end
+
+ it "should assign original fields" do
+ post.update_attributes(title: "Test2")
+ post.update_attributes(title: "Test3")
+ post.history_tracks.where(version: 2).first.original.should == {
+ "title" => "Test2"
+ }
+ end
+
+ it "should assign modifier" do
+ post.update_attributes(title: "Another Test", modifier: another_user)
+ post.history_tracks.last.modifier.should == another_user
+ end
+ end
+
+ describe "on update embedded 1..N (embeds_many)" do
+ it "should assign version on comment" do
+ comment.update_attributes(title: "Test2")
+ comment.version.should == 2 # first track generated on creation
+ end
+
+ it "should create a history track of version 2" do
+ comment.update_attributes(title: "Test2")
+ comment.history_tracks.where(version: 2).first.should_not be_nil
+ end
+
+ it "should assign modified fields" do
+ comment.update_attributes(t: "Test2")
+ comment.history_tracks.where(version: 2).first.modified.should == {
+ "t" => "Test2"
+ }
+ end
+
+ it "should assign original fields" do
+ comment.update_attributes(title: "Test2")
+ comment.history_tracks.where(version: 2).first.original.should == {
+ "t" => "test"
+ }
+ end
+
+ it "should be possible to undo from parent" do
+ comment.update_attributes(title: "Test 2")
+ user
+ post.history_tracks.last.undo!(user)
+ comment.reload
+ comment.title.should == "test"
+ end
+
+ it "should assign modifier" do
+ post.update_attributes(title: "Another Test", modifier: another_user)
+ post.history_tracks.last.modifier.should == another_user
+ end
+ end
+
+ describe "on update embedded 1..1 (embeds_one)" do
+ let(:section) { Section.new(title: 'Technology') }
+
+ before(:each) do
+ post.section = section
+ post.save!
+ post.reload
+ post.section
+ end
+
+ it "should assign version on create section" do
+ section.version.should == 1
+ end
+
+ it "should assign version on section" do
+ section.update_attributes(title: 'Technology 2')
+ section.version.should == 2 # first track generated on creation
+ end
+
+ it "should create a history track of version 2" do
+ section.update_attributes(title: 'Technology 2')
+ section.history_tracks.where(version: 2).first.should_not be_nil
+ end
+
+ it "should assign modified fields" do
+ section.update_attributes(title: 'Technology 2')
+ section.history_tracks.where(version: 2).first.modified.should == {
+ "t" => "Technology 2"
+ }
+ end
+
+ it "should assign original fields" do
+ section.update_attributes(title: 'Technology 2')
+ section.history_tracks.where(version: 2).first.original.should == {
+ "t" => "Technology"
+ }
+ end
+
+ it "should be possible to undo from parent" do
+ section.update_attributes(title: 'Technology 2')
+ post.history_tracks.last.undo!(user)
+ section.reload
+ section.title.should == "Technology"
+ end
+
+ it "should assign modifier" do
+ section.update_attributes(title: "Business", modifier: another_user)
+ post.history_tracks.last.modifier.should == another_user
+ end
+ end
+
+ describe "on destroy embedded" do
+ it "should be possible to re-create destroyed embedded" do
+ comment.destroy
+ comment.history_tracks.last.undo!(user)
+ post.reload
+ post.comments.first.title.should == "test"
+ end
+
+ it "should be possible to re-create destroyed embedded from parent" do
+ comment.destroy
+ post.history_tracks.last.undo!(user)
+ post.reload
+ post.comments.first.title.should == "test"
+ end
+
+ it "should be possible to destroy after re-create embedded from parent" do
+ comment.destroy
+ post.history_tracks.last.undo!(user)
+ post.history_tracks.last.undo!(user)
+ post.reload
+ post.comments.count.should == 0
+ end
+
+ it "should be possible to create with redo after undo create embedded from parent" do
+ comment # initialize
+ post.comments.create!(title: "The second one")
+ track = post.history_tracks.last
+ track.undo!(user)
+ track.redo!(user)
+ post.reload
+ post.comments.count.should == 2
+ end
+ end
+
+ describe "embedded with cascading callbacks" do
+
+ let(:tag_foo) { post.tags.create(title: "foo", updated_by: user) }
+ let(:tag_bar) { post.tags.create(title: "bar") }
+
+ # it "should have cascaded the creation callbacks and set timestamps" do
+ # tag_foo; tag_bar # initialize
+ # tag_foo.created_at.should_not be_nil
+ # tag_foo.updated_at.should_not be_nil
+ # end
+
+ it "should allow an update through the parent model" do
+ update_hash = { "post" => { "tags_attributes" => { "1234" => { "id" => tag_bar.id, "title" => "baz" } } } }
+ post.update_attributes(update_hash["post"])
+ post.tags.last.title.should == "baz"
+ end
+
+ it "should be possible to destroy through parent model using canoncial _destroy macro" do
+ tag_foo
+ tag_bar # initialize
+ post.tags.count.should == 2
+ update_hash = { "post" => { "tags_attributes" => { "1234" => { "id" => tag_bar.id, "title" => "baz", "_destroy" => "true" } } } }
+ post.update_attributes(update_hash["post"])
+ post.tags.count.should == 1
+ post.history_tracks.to_a.last.action.should == "destroy"
+ end
+
+ it "should write relationship name for association_chain hiearchy instead of class name when using _destroy macro" do
+ update_hash = { "tags_attributes" => { "1234" => { "id" => tag_foo.id, "_destroy" => "1" } } }
+ post.update_attributes(update_hash)
+
+ # historically this would have evaluated to 'Tags' and an error would be thrown
+ # on any call that walked up the association_chain, e.g. 'trackable'
+ tag_foo.history_tracks.last.association_chain.last["name"].should == "tags"
+ lambda { tag_foo.history_tracks.last.trackable }.should_not raise_error
+ end
+ end
+
+ describe "non-embedded" do
+ it "should undo changes" do
+ post.update_attributes(title: "Test2")
+ post.history_tracks.where(version: 1).last.undo!(user)
+ post.reload
+ post.title.should == "Test"
+ end
+
+ it "should undo destruction" do
+ post.destroy
+ post.history_tracks.where(version: 1).last.undo!(user)
+ Post.find(post.id).title.should == "Test"
+ end
+
+ it "should create a new history track after undo" do
+ comment # initialize
+ post.update_attributes(title: "Test2")
+ post.history_tracks.last.undo!(user)
+ post.reload
+ post.history_tracks.count.should == 3
+ end
+
+ it "should assign user as the modifier of the newly created history track" do
+ post.update_attributes(title: "Test2")
+ post.history_tracks.where(version: 1).last.undo!(user)
+ post.reload
+ post.history_tracks.where(version: 2).last.modifier.should == user
+ end
+
+ it "should stay the same after undo and redo" do
+ post.update_attributes(title: "Test2")
+ track = post.history_tracks.last
+ track.undo!(user)
+ track.redo!(user)
+ post2 = Post.where(_id: post.id).first
+
+ post.title.should == post2.title
+ end
+
+ it "should be destroyed after undo and redo" do
+ post.destroy
+ track = post.history_tracks.where(version: 1).last
+ track.undo!(user)
+ track.redo!(user)
+ Post.where(_id: post.id).first.should be_nil
+ end
+ end
+
+ describe "embedded" do
+ it "should undo changes" do
+ comment.update_attributes(title: "Test2")
+ comment.history_tracks.where(version: 2).first.undo!(user)
+ comment.reload
+ comment.title.should == "test"
+ end
+
+ it "should create a new history track after undo" do
+ comment.update_attributes(title: "Test2")
+ comment.history_tracks.where(version: 2).first.undo!(user)
+ comment.reload
+ comment.history_tracks.count.should == 3
+ end
+
+ it "should assign user as the modifier of the newly created history track" do
+ comment.update_attributes(title: "Test2")
+ comment.history_tracks.where(version: 2).first.undo!(user)
+ comment.reload
+ comment.history_tracks.where(version: 3).first.modifier.should == user
+ end
+
+ it "should stay the same after undo and redo" do
+ comment.update_attributes(title: "Test2")
+ track = comment.history_tracks.where(version: 2).first
+ track.undo!(user)
+ track.redo!(user)
+ comment.reload
+ comment.title.should == "Test2"
+ end
+ end
+
+ describe "trackables" do
+ before :each do
+ comment.update_attributes(title: "Test2") # version == 2
+ comment.update_attributes(title: "Test3") # version == 3
+ comment.update_attributes(title: "Test4") # version == 4
+ end
+
+ describe "undo" do
+ it "should recognize :from, :to options" do
+ comment.undo! user, from: 4, to: 2
+ comment.title.should == "test"
+ end
+
+ it "should recognize parameter as version number" do
+ comment.undo! user, 3
+ comment.title.should == "Test2"
+ end
+
+ it "should undo last version when no parameter is specified" do
+ comment.undo! user
+ comment.title.should == "Test3"
+ end
+
+ it "should recognize :last options" do
+ comment.undo! user, last: 2
+ comment.title.should == "Test2"
+ end
+
+ context "protected attributes" do
+ before :each do
+ Comment.attr_accessible(nil)
+ end
+
+ after :each do
+ Comment.attr_protected(nil)
+ end
+
+ it "should undo last version when no parameter is specified on protected attributes" do
+ comment.undo! user
+ comment.title.should == "Test3"
+ end
+
+ it "should recognize :last options on model with protected attributes" do
+ comment.undo! user, last: 2
+ comment.title.should == "Test2"
+ end
+ end
+ end
+
+ describe "redo" do
+ before :each do
+ comment.update_attributes(title: "Test5")
+ end
+
+ it "should recognize :from, :to options" do
+ comment.redo! user, from: 2, to: 4
+ comment.title.should == "Test4"
+ end
+
+ it "should recognize parameter as version number" do
+ comment.redo! user, 2
+ comment.title.should == "Test2"
+ end
+
+ it "should redo last version when no parameter is specified" do
+ comment.redo! user
+ comment.title.should == "Test5"
+ end
+
+ it "should recognize :last options" do
+ comment.redo! user, last: 1
+ comment.title.should == "Test5"
+ end
+
+ context "protected attributes" do
+ before :each do
+ Comment.attr_accessible(nil)
+ end
+
+ after :each do
+ Comment.attr_protected(nil)
+ end
+
+ it "should recognize parameter as version number" do
+ comment.redo! user, 2
+ comment.title.should == "Test2"
+ end
+
+ it "should recognize :from, :to options" do
+ comment.redo! user, from: 2, to: 4
+ comment.title.should == "Test4"
+ end
+ end
+
+ end
+ end
+
+ describe "localized fields" do
+ before :each do
+ class Sausage
+ include Mongoid::Document
+ include Mongoid::History::Trackable
+
+ field :flavour, localize: true
+ track_history on: [:flavour], track_destroy: true
+ end
+ end
+ it "should correctly undo and redo" do
+ if Sausage.respond_to?(:localized_fields)
+ sausage = Sausage.create(flavour_translations: { 'en' => "Apple", 'nl' => 'Appel' })
+ sausage.update_attributes(flavour: "Guinness")
+
+ track = sausage.history_tracks.last
+
+ track.undo! user
+ sausage.reload.flavour.should == "Apple"
+
+ track.redo! user
+ sausage.reload.flavour.should == "Guinness"
+
+ sausage.destroy
+ sausage.history_tracks.last.action.should == "destroy"
+ sausage.history_tracks.last.undo! user
+ sausage.reload.flavour.should == "Guinness"
+ end
+ end
+ end
+
+ describe "embedded with a polymorphic trackable" do
+ let(:foo) { Foo.new(title: 'a title', body: 'a body') }
+ before :each do
+ post.comments << foo
+ post.save
+ end
+ it "should assign interface name in association chain" do
+ foo.update_attribute(:body, 'a changed body')
+ expected_root = { "name" => "Post", "id" => post.id }
+ expected_node = { "name" => "coms", "id" => foo.id }
+ foo.history_tracks.first.association_chain.should == [expected_root, expected_node]
+ end
+ end
+
+ describe "#trackable_parent_class" do
+ context "a non-embedded model" do
+ it "should return the trackable parent class" do
+ tag.history_tracks.first.trackable_parent_class.should == Tag
+ end
+ it "should return the parent class even if the trackable is deleted" do
+ tracker = tag.history_tracks.first
+ tag.destroy
+ tracker.trackable_parent_class.should == Tag
+ end
+ end
+ context "an embedded model" do
+ it "should return the trackable parent class" do
+ comment.update_attributes(title: "Foo")
+ comment.history_tracks.first.trackable_parent_class.should == Post
+ end
+ it "should return the parent class even if the trackable is deleted" do
+ tracker = comment.history_tracks.first
+ comment.destroy
+ tracker.trackable_parent_class.should == Post
+ end
+ end
+ end
+ end
+end