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