spec/integration/integration_spec.rb in mongoid-history-0.8.3 vs spec/integration/integration_spec.rb in mongoid-history-0.8.5
- old
+ new
@@ -1,976 +1,976 @@
-require 'spec_helper'
-
-describe Mongoid::History do
- before :each 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: %i[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
- # BUG: see https://github.com/mongoid/mongoid-history/issues/223, modifier_field_optional should not be necessary
- track_history on: %i[title body], scope: :post, track_create: true, track_destroy: true, modifier_field_optional: 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: %i[email updated_at], modifier_field_optional: true
- 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
- end
-
- after :each do
- Object.send(:remove_const, :Post)
- Object.send(:remove_const, :Comment)
- Object.send(:remove_const, :Section)
- Object.send(:remove_const, :User)
- Object.send(:remove_const, :Tag)
- Object.send(:remove_const, :Foo)
- end
-
- 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', updated_by: user) }
-
- describe 'track' do
- describe 'on creation' do
- it 'should have one history track in comment' do
- expect(comment.history_tracks.count).to eq(1)
- end
-
- it 'should assign title and body on modified' do
- expect(comment.history_tracks.first.modified).to eq('t' => 'test', 'body' => 'comment')
- end
-
- it 'should not assign title and body on original' do
- expect(comment.history_tracks.first.original).to eq({})
- end
-
- it 'should assign modifier' do
- expect(comment.history_tracks.first.modifier.id).to eq(user.id)
- end
-
- it 'should assign version' do
- expect(comment.history_tracks.first.version).to eq(1)
- end
-
- it 'should assign scope' do
- expect(comment.history_tracks.first.scope).to eq('post')
- end
-
- it 'should assign method' do
- expect(comment.history_tracks.first.action).to eq('create')
- end
-
- it 'should assign association_chain' do
- expected = [
- { 'id' => post.id, 'name' => 'Post' },
- { 'id' => comment.id, 'name' => 'coms' }
- ]
- expect(comment.history_tracks.first.association_chain).to eq(expected)
- end
- end
-
- describe 'on destruction' do
- it 'should have two history track records in post' do
- post # This will create history track records for creation
- expect do
- post.destroy
- end.to change(Tracker, :count).by(1)
- end
-
- it 'should assign destroy on track record' do
- post.destroy
- expect(post.history_tracks.last.action).to eq('destroy')
- end
-
- it 'should return affected attributes from track record' do
- post.destroy
- expect(post.history_tracks.last.affected['title']).to eq('Test')
- end
-
- it 'should no-op on repeated calls to destroy' do
- post.destroy
- expect do
- post.destroy
- end.not_to change(Tracker, :count)
- end
- end
-
- describe 'on update non-embedded' do
- it 'should create a history track if changed attributes match tracked attributes' do
- post # This will create history track records for creation
- expect do
- post.update_attributes!(title: 'Another Test')
- end.to change(Tracker, :count).by(1)
- end
-
- it 'should not create a history track if changed attributes do not match tracked attributes' do
- post # This will create history track records for creation
- expect do
- post.update_attributes!(rating: 'untracked')
- end.to change(Tracker, :count).by(0)
- end
-
- it 'should assign modified fields' do
- post.update_attributes!(title: 'Another Test')
- expect(post.history_tracks.last.modified).to eq(
- 'title' => 'Another Test'
- )
- end
-
- it 'should assign method field' do
- post.update_attributes!(title: 'Another Test')
- expect(post.history_tracks.last.action).to eq('update')
- end
-
- it 'should assign original fields' do
- post.update_attributes!(title: 'Another Test')
- expect(post.history_tracks.last.original).to eq(
- 'title' => 'Test'
- )
- end
-
- it 'should assign modifier' do
- post.update_attributes!(title: 'Another Test')
- expect(post.history_tracks.first.modifier.id).to eq(user.id)
- end
-
- it 'should assign version on history tracks' do
- post.update_attributes!(title: 'Another Test')
- expect(post.history_tracks.first.version).to eq(1)
- end
-
- it 'should assign version on post' do
- expect(post.version).to eq(1) # Created
- post.update_attributes!(title: 'Another Test')
- expect(post.version).to eq(2) # Updated
- end
-
- it 'should assign scope' do
- post.update_attributes!(title: 'Another Test')
- expect(post.history_tracks.first.scope).to eq('post')
- end
-
- it 'should assign association_chain' do
- post.update_attributes!(title: 'Another Test')
- expect(post.history_tracks.last.association_chain).to eq([{ 'id' => post.id, 'name' => 'Post' }])
- end
-
- it 'should exclude defined options' do
- name = user.name
- user.update_attributes!(name: 'Aaron2', email: 'aaronsnewemail@randomemail.com')
- expect(user.history_tracks.last.original.keys).to eq(['n'])
- expect(user.history_tracks.last.original['n']).to eq(name)
- expect(user.history_tracks.last.modified.keys).to eq(['n'])
- expect(user.history_tracks.last.modified['n']).to eq(user.name)
- end
-
- it 'should undo field changes' do
- name = user.name
- user.update_attributes!(name: 'Aaron2', email: 'aaronsnewemail@randomemail.com')
- user.history_tracks.last.undo! nil
- expect(user.reload.name).to eq(name)
- end
-
- it 'should undo non-existing field changes' do
- post = Post.create!(modifier: user, views: 100)
- expect(post.reload.title).to be_nil
- post.update_attributes!(title: 'Aaron2')
- expect(post.reload.title).to eq('Aaron2')
- post.history_tracks.last.undo! user
- expect(post.reload.title).to be_nil
- end
-
- it 'should track array changes' do
- aliases = user.aliases
- user.update_attributes!(aliases: %w[bob joe])
- expect(user.history_tracks.last.original['aliases']).to eq(aliases)
- expect(user.history_tracks.last.modified['aliases']).to eq(user.aliases)
- end
-
- it 'should undo array changes' do
- aliases = user.aliases
- user.update_attributes!(aliases: %w[bob joe])
- user.history_tracks.last.undo! nil
- expect(user.reload.aliases).to eq(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
- expect(subject[:title]).to eq({ to: 'test' }.with_indifferent_access)
- end
- end
- context 'destroy action' do
- subject do
- tag.destroy
- tag.history_tracks.last.tracked_changes
- end
- it 'consider all fields values as :from' do
- expect(subject[:title]).to eq({ from: 'test' }.with_indifferent_access)
- end
- end
- context 'update action' do
- subject { user.history_tracks.last.tracked_changes }
- before do
- user.update_attributes!(name: 'Aaron2', email: nil, country: '', city: nil, phone: '867-5309', aliases: ['', 'bill', 'james'])
- end
- it { is_expected.to be_a HashWithIndifferentAccess }
- it 'should track changed field' do
- expect(subject[:n]).to eq({ from: 'Aaron', to: 'Aaron2' }.with_indifferent_access)
- end
- it 'should track added field' do
- expect(subject[:phone]).to eq({ to: '867-5309' }.with_indifferent_access)
- end
- it 'should track removed field' do
- expect(subject[:city]).to eq({ from: 'Toronto' }.with_indifferent_access)
- end
- it 'should not consider blank as removed' do
- expect(subject[:country]).to eq({ from: 'Canada', to: '' }.with_indifferent_access)
- end
- it 'should track changed array field' do
- expect(subject[:aliases]).to eq({ from: ['bob'], to: ['', 'bill', 'james'] }.with_indifferent_access)
- end
- it 'should not track unmodified field' do
- expect(subject[:address]).to be_nil
- end
- it 'should not track untracked fields' do
- expect(subject[:email]).to 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
- expect(subject[:add]).to eq({ title: 'test' }.with_indifferent_access)
- end
- end
- context 'destroy action' do
- subject do
- tag.destroy
- tag.history_tracks.last.tracked_edits
- end
- it 'consider all edits as ;remove' do
- expect(subject[:remove]).to eq({ title: 'test' }.with_indifferent_access)
- end
- end
- context 'update action' do
- subject { user.history_tracks.last.tracked_edits }
- before do
- user.update_attributes!(name: 'Aaron2', email: nil, country: '', city: nil, phone: '867-5309', aliases: ['', 'bill', 'james'])
- end
- it { is_expected.to be_a HashWithIndifferentAccess }
- it 'should track changed field' do
- expect(subject[:modify]).to eq({ n: { from: 'Aaron', to: 'Aaron2' } }.with_indifferent_access)
- end
- it 'should track added field' do
- expect(subject[:add]).to eq({ phone: '867-5309' }.with_indifferent_access)
- end
- it 'should track removed field and consider blank as removed' do
- expect(subject[:remove]).to eq({ city: 'Toronto', country: 'Canada' }.with_indifferent_access)
- end
- it 'should track changed array field' do
- expect(subject[:array]).to eq({ 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|
- expect(subject[edit][:address]).to be_nil
- end
- end
- it 'should not track untracked fields' do
- %w[add modify remove array].each do |edit|
- expect(subject[edit][:email]).to be_nil
- end
- end
- end
- context 'with empty values' do
- before do
- allow(subject).to receive(:trackable_parent_class) { Tracker }
- allow(Tracker).to receive(:tracked_embeds_many?) { false }
- end
- subject { Tracker.new }
- it 'should skip empty values' do
- allow(subject).to receive(:tracked_changes) { { name: { to: '', from: [] }, city: { to: 'Toronto', from: '' } } }
- expect(subject.tracked_edits).to eq({ add: { city: 'Toronto' } }.with_indifferent_access)
- end
- end
- end
-
- describe 'on update non-embedded twice' do
- it 'should assign version on post' do
- expect(post.version).to eq(1)
- post.update_attributes!(title: 'Test2')
- post.update_attributes!(title: 'Test3')
- expect(post.version).to eq(3)
- end
-
- it 'should create a history track if changed attributes match tracked attributes' do
- post # Created
- expect do
- post.update_attributes!(title: 'Test2')
- post.update_attributes!(title: 'Test3')
- end.to 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')
- expect(post.history_tracks.where(version: 2).first).not_to be_nil
- end
-
- it 'should assign modified fields' do
- post.update_attributes!(title: 'Test2')
- post.update_attributes!(title: 'Test3')
- expect(post.history_tracks.where(version: 3).first.modified).to eq(
- 'title' => 'Test3'
- )
- end
-
- it 'should assign original fields' do
- post.update_attributes!(title: 'Test2')
- post.update_attributes!(title: 'Test3')
- expect(post.history_tracks.where(version: 3).first.original).to eq(
- 'title' => 'Test2'
- )
- end
-
- it 'should assign modifier' do
- post.update_attributes!(title: 'Another Test', modifier: another_user)
- expect(post.history_tracks.last.modifier.id).to eq(another_user.id)
- end
- end
-
- describe 'on update embedded 1..N (embeds_many)' do
- it 'should assign version on comment' do
- comment.update_attributes!(title: 'Test2')
- expect(comment.version).to eq(2) # first track generated on creation
- end
-
- it 'should create a history track of version 2' do
- comment.update_attributes!(title: 'Test2')
- expect(comment.history_tracks.where(version: 2).first).not_to be_nil
- end
-
- it 'should assign modified fields' do
- comment.update_attributes!(t: 'Test2')
- expect(comment.history_tracks.where(version: 2).first.modified).to eq(
- 't' => 'Test2'
- )
- end
-
- it 'should assign original fields' do
- comment.update_attributes!(title: 'Test2')
- expect(comment.history_tracks.where(version: 2).first.original).to eq(
- '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
- expect(comment.title).to eq('test')
- end
-
- it 'should assign modifier' do
- post.update_attributes!(title: 'Another Test', modifier: another_user)
- expect(post.history_tracks.last.modifier.id).to eq(another_user.id)
- end
- end
-
- describe 'on update embedded 1..1 (embeds_one)' do
- let(:section) { Section.new(title: 'Technology', modifier: user) }
-
- before(:each) do
- post.section = section
- post.modifier = user
- post.save!
- post.reload
- post.section
- end
-
- it 'should assign version on create section' do
- expect(section.version).to eq(1)
- end
-
- it 'should assign version on section' do
- section.update_attributes!(title: 'Technology 2')
- expect(section.version).to eq(2) # first track generated on creation
- end
-
- it 'should create a history track of version 2' do
- section.update_attributes!(title: 'Technology 2')
- expect(section.history_tracks.where(version: 2).first).not_to be_nil
- end
-
- it 'should assign modified fields' do
- section.update_attributes!(title: 'Technology 2')
- expect(section.history_tracks.where(version: 2).first.modified).to eq(
- 't' => 'Technology 2'
- )
- end
-
- it 'should assign original fields' do
- section.update_attributes!(title: 'Technology 2')
- expect(section.history_tracks.where(version: 2).first.original).to eq(
- '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
- expect(section.title).to eq('Technology')
- end
-
- it 'should assign modifier' do
- section.update_attributes!(title: 'Business', modifier: another_user)
- expect(post.history_tracks.last.modifier.id).to eq(another_user.id)
- 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
- expect(post.comments.first.title).to eq('test')
- end
-
- it 'should be possible to re-create destroyed embedded from parent' do
- comment.destroy
- post.history_tracks.last.undo!(user)
- post.reload
- expect(post.comments.first.title).to eq('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
- expect(post.comments.count).to eq(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', modifier: user)
- track = post.history_tracks[2]
- expect(post.reload.comments.count).to eq 2
- track.undo!(user)
- expect(post.reload.comments.count).to eq 1
- track.redo!(user)
- expect(post.reload.comments.count).to eq 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', updated_by: user) }
-
- 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'])
- expect(post.tags.last.title).to eq('baz')
- end
-
- it 'should be possible to destroy through parent model using canoncial _destroy macro' do
- tag_foo
- tag_bar # initialize
- expect(post.tags.count).to eq(2)
- update_hash = { 'post' => { 'tags_attributes' => { '1234' => { 'id' => tag_bar.id, 'title' => 'baz', '_destroy' => 'true' } } } }
- post.update_attributes!(update_hash['post'])
- expect(post.tags.count).to eq(1)
- expect(post.history_tracks.to_a.last.action).to eq('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'
- expect(tag_foo.history_tracks.last.association_chain.last['name']).to eq('tags')
- expect { tag_foo.history_tracks.last.trackable }.not_to raise_error
- end
- end
-
- describe 'non-embedded' do
- it 'should undo changes' do
- post.update_attributes!(title: 'Test2')
- post.history_tracks.where(version: 2).last.undo!(user)
- post.reload
- expect(post.title).to eq('Test')
- end
-
- it 'should undo destruction' do
- post.destroy
- post.history_tracks.where(version: 2).last.undo!(user)
- expect(Post.find(post.id).title).to eq('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
- expect(post.history_tracks.count).to eq(4)
- 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: 2).last.undo!(user)
- post.reload
- expect(post.history_tracks.where(version: 2).last.modifier.id).to eq(user.id)
- 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
-
- expect(post.title).to eq(post2.title)
- end
-
- it 'should be destroyed after undo and redo' do
- post.destroy
- track = post.history_tracks.where(version: 2).last
- track.undo!(user)
- track.redo!(user)
- expect(Post.where(_id: post.id).first).to 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
- expect(comment.title).to eq('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
- expect(comment.history_tracks.count).to eq(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
- expect(comment.history_tracks.where(version: 3).first.modifier.id).to eq(user.id)
- 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
- expect(comment.title).to eq('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
- { 'undo' => [nil], 'undo!' => [nil, :reload] }.each do |test_method, methods|
- methods.each do |method|
- context (method || 'instance').to_s do
- it 'recognizes :from, :to options' do
- comment.send test_method, user, from: 4, to: 2
- comment.send(method) if method
- expect(comment.title).to eq('test')
- end
-
- it 'recognizes parameter as version number' do
- comment.send test_method, user, 3
- comment.send(method) if method
- expect(comment.title).to eq('Test2')
- end
-
- it 'should undo last version when no parameter is specified' do
- comment.send test_method, user
- comment.send(method) if method
- expect(comment.title).to eq('Test3')
- end
-
- it 'recognizes :last options' do
- comment.send test_method, user, last: 2
- comment.send(method) if method
- expect(comment.title).to eq('Test2')
- end
-
- if Mongoid::Compatibility::Version.mongoid3?
- 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.send test_method, user
- comment.send(method) if method
- expect(comment.title).to eq('Test3')
- end
-
- it 'recognizes :last options on model with protected attributes' do
- comment.send test_method, user, last: 2
- comment.send(method) if method
- expect(comment.title).to eq('Test2')
- end
- end
- end
- end
- end
- end
- end
-
- describe 'redo' do
- [nil, :reload].each do |method|
- context (method || 'instance').to_s 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.send(method) if method
- expect(comment.title).to eq('Test4')
- end
-
- it 'should recognize parameter as version number' do
- comment.redo! user, 2
- comment.send(method) if method
- expect(comment.title).to eq('Test2')
- end
-
- it 'should redo last version when no parameter is specified' do
- comment.redo! user
- comment.send(method) if method
- expect(comment.title).to eq('Test5')
- end
-
- it 'should recognize :last options' do
- comment.redo! user, last: 1
- comment.send(method) if method
- expect(comment.title).to eq('Test5')
- end
-
- if Mongoid::Compatibility::Version.mongoid3?
- 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.send(method) if method
- expect(comment.title).to eq('Test2')
- end
-
- it 'should recognize :from, :to options' do
- comment.redo! user, from: 2, to: 4
- comment.send(method) if method
- expect(comment.title).to eq('Test4')
- end
- end
- end
- end
- end
- end
- end
-
- describe 'embedded with a polymorphic trackable' do
- let(:foo) { Foo.new(title: 'a title', body: 'a body', modifier: user) }
- 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 }
- expect(foo.history_tracks.first.association_chain).to eq([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
- expect(tag.history_tracks.first.trackable_parent_class).to eq(Tag)
- end
- it 'should return the parent class even if the trackable is deleted' do
- tracker = tag.history_tracks.first
- tag.destroy
- expect(tracker.trackable_parent_class).to eq(Tag)
- end
- end
- context 'an embedded model' do
- it 'should return the trackable parent class' do
- comment.update_attributes!(title: 'Foo')
- expect(comment.history_tracks.first.trackable_parent_class).to eq(Post)
- end
- it 'should return the parent class even if the trackable is deleted' do
- tracker = comment.history_tracks.first
- comment.destroy
- expect(tracker.trackable_parent_class).to eq(Post)
- end
- end
- end
-
- describe 'when default scope is present' do
- before :each do
- class Post
- default_scope -> { where(title: nil) }
- end
- class Comment
- default_scope -> { where(title: nil) }
- end
- class User
- default_scope -> { where(name: nil) }
- end
- class Tag
- default_scope -> { where(title: nil) }
- end
- end
-
- describe 'post' do
- it 'should correctly undo and redo' do
- post.update_attributes!(title: 'a new title')
- track = post.history_tracks.last
- track.undo! user
- expect(post.reload.title).to eq('Test')
- track.redo! user
- expect(post.reload.title).to eq('a new title')
- end
-
- it 'should stay the same after undo and redo' do
- post.update_attributes!(title: 'testing')
- track = post.history_tracks.last
- track.undo! user
- track.redo! user
- expect(post.reload.title).to eq('testing')
- end
- end
- describe 'comment' do
- it 'should correctly undo and redo' do
- comment.update_attributes!(title: 'a new title')
- track = comment.history_tracks.last
- track.undo! user
- expect(comment.reload.title).to eq('test')
- track.redo! user
- expect(comment.reload.title).to eq('a new title')
- end
-
- it 'should stay the same after undo and redo' do
- comment.update_attributes!(title: 'testing')
- track = comment.history_tracks.last
- track.undo! user
- track.redo! user
- expect(comment.reload.title).to eq('testing')
- end
- end
- describe 'user' do
- it 'should correctly undo and redo' do
- user.update_attributes!(name: 'a new name')
- track = user.history_tracks.last
- track.undo! user
- expect(user.reload.name).to eq('Aaron')
- track.redo! user
- expect(user.reload.name).to eq('a new name')
- end
-
- it 'should stay the same after undo and redo' do
- user.update_attributes!(name: 'testing')
- track = user.history_tracks.last
- track.undo! user
- track.redo! user
- expect(user.reload.name).to eq('testing')
- end
- end
- describe 'tag' do
- it 'should correctly undo and redo' do
- tag.update_attributes!(title: 'a new title')
- track = tag.history_tracks.last
- track.undo! user
- expect(tag.reload.title).to eq('test')
- track.redo! user
- expect(tag.reload.title).to eq('a new title')
- end
-
- it 'should stay the same after undo and redo' do
- tag.update_attributes!(title: 'testing')
- track = tag.history_tracks.last
- track.undo! user
- track.redo! user
- expect(tag.reload.title).to eq('testing')
- end
- end
- end
-
- describe 'overriden changes_method with additional fields' do
- before :each do
- class OverriddenChangesMethod
- include Mongoid::Document
- include Mongoid::History::Trackable
-
- track_history on: [:foo], changes_method: :my_changes
-
- def my_changes
- { foo: %w[bar baz] }
- end
- end
- end
-
- after :each do
- Object.send(:remove_const, :OverriddenChangesMethod)
- end
-
- it 'should add foo to the changes history' do
- o = OverriddenChangesMethod.create(modifier: user)
- o.save!
- track = o.history_tracks.last
- expect(track.modified).to eq('foo' => 'baz')
- expect(track.original).to eq('foo' => 'bar')
- 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, modifier_field_optional: true
- end
- end
-
- after :each do
- Object.send(:remove_const, :Sausage)
- end
-
- it 'should correctly undo and redo' do
- pending unless Sausage.respond_to?(:localized_fields)
-
- sausage = Sausage.create!(flavour_translations: { 'en' => 'Apple', 'nl' => 'Appel' }, modifier: user)
- sausage.update_attributes!(flavour: 'Guinness')
-
- track = sausage.history_tracks.last
-
- track.undo! user
- expect(sausage.reload.flavour).to eq('Apple')
-
- track.redo! user
- expect(sausage.reload.flavour).to eq('Guinness')
-
- sausage.destroy
- expect(sausage.history_tracks.last.action).to eq('destroy')
- sausage.history_tracks.last.undo! user
- expect(sausage.reload.flavour).to eq('Guinness')
- end
- end
-
- describe 'changing collection' do
- before :each do
- class Fish
- include Mongoid::Document
- include Mongoid::History::Trackable
-
- track_history on: [:species], modifier_field_optional: true
- store_in collection: :animals
-
- field :species
- end
- end
-
- after :each do
- Object.send(:remove_const, :Fish)
- end
-
- it 'should track history' do
- Fish.new.save!
- end
- end
- end
-end
+require 'spec_helper'
+
+describe Mongoid::History do
+ before :each 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: %i[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
+ # BUG: see https://github.com/mongoid/mongoid-history/issues/223, modifier_field_optional should not be necessary
+ track_history on: %i[title body], scope: :post, track_create: true, track_destroy: true, modifier_field_optional: 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: %i[email updated_at], modifier_field_optional: true
+ 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
+ end
+
+ after :each do
+ Object.send(:remove_const, :Post)
+ Object.send(:remove_const, :Comment)
+ Object.send(:remove_const, :Section)
+ Object.send(:remove_const, :User)
+ Object.send(:remove_const, :Tag)
+ Object.send(:remove_const, :Foo)
+ end
+
+ 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', updated_by: user) }
+
+ describe 'track' do
+ describe 'on creation' do
+ it 'should have one history track in comment' do
+ expect(comment.history_tracks.count).to eq(1)
+ end
+
+ it 'should assign title and body on modified' do
+ expect(comment.history_tracks.first.modified).to eq('t' => 'test', 'body' => 'comment')
+ end
+
+ it 'should not assign title and body on original' do
+ expect(comment.history_tracks.first.original).to eq({})
+ end
+
+ it 'should assign modifier' do
+ expect(comment.history_tracks.first.modifier.id).to eq(user.id)
+ end
+
+ it 'should assign version' do
+ expect(comment.history_tracks.first.version).to eq(1)
+ end
+
+ it 'should assign scope' do
+ expect(comment.history_tracks.first.scope).to eq('post')
+ end
+
+ it 'should assign method' do
+ expect(comment.history_tracks.first.action).to eq('create')
+ end
+
+ it 'should assign association_chain' do
+ expected = [
+ { 'id' => post.id, 'name' => 'Post' },
+ { 'id' => comment.id, 'name' => 'coms' }
+ ]
+ expect(comment.history_tracks.first.association_chain).to eq(expected)
+ end
+ end
+
+ describe 'on destruction' do
+ it 'should have two history track records in post' do
+ post # This will create history track records for creation
+ expect do
+ post.destroy
+ end.to change(Tracker, :count).by(1)
+ end
+
+ it 'should assign destroy on track record' do
+ post.destroy
+ expect(post.history_tracks.last.action).to eq('destroy')
+ end
+
+ it 'should return affected attributes from track record' do
+ post.destroy
+ expect(post.history_tracks.last.affected['title']).to eq('Test')
+ end
+
+ it 'should no-op on repeated calls to destroy' do
+ post.destroy
+ expect do
+ post.destroy
+ end.not_to change(Tracker, :count)
+ end
+ end
+
+ describe 'on update non-embedded' do
+ it 'should create a history track if changed attributes match tracked attributes' do
+ post # This will create history track records for creation
+ expect do
+ post.update_attributes!(title: 'Another Test')
+ end.to change(Tracker, :count).by(1)
+ end
+
+ it 'should not create a history track if changed attributes do not match tracked attributes' do
+ post # This will create history track records for creation
+ expect do
+ post.update_attributes!(rating: 'untracked')
+ end.to change(Tracker, :count).by(0)
+ end
+
+ it 'should assign modified fields' do
+ post.update_attributes!(title: 'Another Test')
+ expect(post.history_tracks.last.modified).to eq(
+ 'title' => 'Another Test'
+ )
+ end
+
+ it 'should assign method field' do
+ post.update_attributes!(title: 'Another Test')
+ expect(post.history_tracks.last.action).to eq('update')
+ end
+
+ it 'should assign original fields' do
+ post.update_attributes!(title: 'Another Test')
+ expect(post.history_tracks.last.original).to eq(
+ 'title' => 'Test'
+ )
+ end
+
+ it 'should assign modifier' do
+ post.update_attributes!(title: 'Another Test')
+ expect(post.history_tracks.first.modifier.id).to eq(user.id)
+ end
+
+ it 'should assign version on history tracks' do
+ post.update_attributes!(title: 'Another Test')
+ expect(post.history_tracks.first.version).to eq(1)
+ end
+
+ it 'should assign version on post' do
+ expect(post.version).to eq(1) # Created
+ post.update_attributes!(title: 'Another Test')
+ expect(post.version).to eq(2) # Updated
+ end
+
+ it 'should assign scope' do
+ post.update_attributes!(title: 'Another Test')
+ expect(post.history_tracks.first.scope).to eq('post')
+ end
+
+ it 'should assign association_chain' do
+ post.update_attributes!(title: 'Another Test')
+ expect(post.history_tracks.last.association_chain).to eq([{ 'id' => post.id, 'name' => 'Post' }])
+ end
+
+ it 'should exclude defined options' do
+ name = user.name
+ user.update_attributes!(name: 'Aaron2', email: 'aaronsnewemail@randomemail.com')
+ expect(user.history_tracks.last.original.keys).to eq(['n'])
+ expect(user.history_tracks.last.original['n']).to eq(name)
+ expect(user.history_tracks.last.modified.keys).to eq(['n'])
+ expect(user.history_tracks.last.modified['n']).to eq(user.name)
+ end
+
+ it 'should undo field changes' do
+ name = user.name
+ user.update_attributes!(name: 'Aaron2', email: 'aaronsnewemail@randomemail.com')
+ user.history_tracks.last.undo! nil
+ expect(user.reload.name).to eq(name)
+ end
+
+ it 'should undo non-existing field changes' do
+ post = Post.create!(modifier: user, views: 100)
+ expect(post.reload.title).to be_nil
+ post.update_attributes!(title: 'Aaron2')
+ expect(post.reload.title).to eq('Aaron2')
+ post.history_tracks.last.undo! user
+ expect(post.reload.title).to be_nil
+ end
+
+ it 'should track array changes' do
+ aliases = user.aliases
+ user.update_attributes!(aliases: %w[bob joe])
+ expect(user.history_tracks.last.original['aliases']).to eq(aliases)
+ expect(user.history_tracks.last.modified['aliases']).to eq(user.aliases)
+ end
+
+ it 'should undo array changes' do
+ aliases = user.aliases
+ user.update_attributes!(aliases: %w[bob joe])
+ user.history_tracks.last.undo! nil
+ expect(user.reload.aliases).to eq(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
+ expect(subject[:title]).to eq({ to: 'test' }.with_indifferent_access)
+ end
+ end
+ context 'destroy action' do
+ subject do
+ tag.destroy
+ tag.history_tracks.last.tracked_changes
+ end
+ it 'consider all fields values as :from' do
+ expect(subject[:title]).to eq({ from: 'test' }.with_indifferent_access)
+ end
+ end
+ context 'update action' do
+ subject { user.history_tracks.last.tracked_changes }
+ before do
+ user.update_attributes!(name: 'Aaron2', email: nil, country: '', city: nil, phone: '867-5309', aliases: ['', 'bill', 'james'])
+ end
+ it { is_expected.to be_a HashWithIndifferentAccess }
+ it 'should track changed field' do
+ expect(subject[:n]).to eq({ from: 'Aaron', to: 'Aaron2' }.with_indifferent_access)
+ end
+ it 'should track added field' do
+ expect(subject[:phone]).to eq({ to: '867-5309' }.with_indifferent_access)
+ end
+ it 'should track removed field' do
+ expect(subject[:city]).to eq({ from: 'Toronto' }.with_indifferent_access)
+ end
+ it 'should not consider blank as removed' do
+ expect(subject[:country]).to eq({ from: 'Canada', to: '' }.with_indifferent_access)
+ end
+ it 'should track changed array field' do
+ expect(subject[:aliases]).to eq({ from: ['bob'], to: ['', 'bill', 'james'] }.with_indifferent_access)
+ end
+ it 'should not track unmodified field' do
+ expect(subject[:address]).to be_nil
+ end
+ it 'should not track untracked fields' do
+ expect(subject[:email]).to 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
+ expect(subject[:add]).to eq({ title: 'test' }.with_indifferent_access)
+ end
+ end
+ context 'destroy action' do
+ subject do
+ tag.destroy
+ tag.history_tracks.last.tracked_edits
+ end
+ it 'consider all edits as ;remove' do
+ expect(subject[:remove]).to eq({ title: 'test' }.with_indifferent_access)
+ end
+ end
+ context 'update action' do
+ subject { user.history_tracks.last.tracked_edits }
+ before do
+ user.update_attributes!(name: 'Aaron2', email: nil, country: '', city: nil, phone: '867-5309', aliases: ['', 'bill', 'james'])
+ end
+ it { is_expected.to be_a HashWithIndifferentAccess }
+ it 'should track changed field' do
+ expect(subject[:modify]).to eq({ n: { from: 'Aaron', to: 'Aaron2' } }.with_indifferent_access)
+ end
+ it 'should track added field' do
+ expect(subject[:add]).to eq({ phone: '867-5309' }.with_indifferent_access)
+ end
+ it 'should track removed field and consider blank as removed' do
+ expect(subject[:remove]).to eq({ city: 'Toronto', country: 'Canada' }.with_indifferent_access)
+ end
+ it 'should track changed array field' do
+ expect(subject[:array]).to eq({ 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|
+ expect(subject[edit][:address]).to be_nil
+ end
+ end
+ it 'should not track untracked fields' do
+ %w[add modify remove array].each do |edit|
+ expect(subject[edit][:email]).to be_nil
+ end
+ end
+ end
+ context 'with empty values' do
+ before do
+ allow(subject).to receive(:trackable_parent_class) { Tracker }
+ allow(Tracker).to receive(:tracked_embeds_many?) { false }
+ end
+ subject { Tracker.new }
+ it 'should skip empty values' do
+ allow(subject).to receive(:tracked_changes) { { name: { to: '', from: [] }, city: { to: 'Toronto', from: '' } } }
+ expect(subject.tracked_edits).to eq({ add: { city: 'Toronto' } }.with_indifferent_access)
+ end
+ end
+ end
+
+ describe 'on update non-embedded twice' do
+ it 'should assign version on post' do
+ expect(post.version).to eq(1)
+ post.update_attributes!(title: 'Test2')
+ post.update_attributes!(title: 'Test3')
+ expect(post.version).to eq(3)
+ end
+
+ it 'should create a history track if changed attributes match tracked attributes' do
+ post # Created
+ expect do
+ post.update_attributes!(title: 'Test2')
+ post.update_attributes!(title: 'Test3')
+ end.to 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')
+ expect(post.history_tracks.where(version: 2).first).not_to be_nil
+ end
+
+ it 'should assign modified fields' do
+ post.update_attributes!(title: 'Test2')
+ post.update_attributes!(title: 'Test3')
+ expect(post.history_tracks.where(version: 3).first.modified).to eq(
+ 'title' => 'Test3'
+ )
+ end
+
+ it 'should assign original fields' do
+ post.update_attributes!(title: 'Test2')
+ post.update_attributes!(title: 'Test3')
+ expect(post.history_tracks.where(version: 3).first.original).to eq(
+ 'title' => 'Test2'
+ )
+ end
+
+ it 'should assign modifier' do
+ post.update_attributes!(title: 'Another Test', modifier: another_user)
+ expect(post.history_tracks.last.modifier.id).to eq(another_user.id)
+ end
+ end
+
+ describe 'on update embedded 1..N (embeds_many)' do
+ it 'should assign version on comment' do
+ comment.update_attributes!(title: 'Test2')
+ expect(comment.version).to eq(2) # first track generated on creation
+ end
+
+ it 'should create a history track of version 2' do
+ comment.update_attributes!(title: 'Test2')
+ expect(comment.history_tracks.where(version: 2).first).not_to be_nil
+ end
+
+ it 'should assign modified fields' do
+ comment.update_attributes!(t: 'Test2')
+ expect(comment.history_tracks.where(version: 2).first.modified).to eq(
+ 't' => 'Test2'
+ )
+ end
+
+ it 'should assign original fields' do
+ comment.update_attributes!(title: 'Test2')
+ expect(comment.history_tracks.where(version: 2).first.original).to eq(
+ '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
+ expect(comment.title).to eq('test')
+ end
+
+ it 'should assign modifier' do
+ post.update_attributes!(title: 'Another Test', modifier: another_user)
+ expect(post.history_tracks.last.modifier.id).to eq(another_user.id)
+ end
+ end
+
+ describe 'on update embedded 1..1 (embeds_one)' do
+ let(:section) { Section.new(title: 'Technology', modifier: user) }
+
+ before(:each) do
+ post.section = section
+ post.modifier = user
+ post.save!
+ post.reload
+ post.section
+ end
+
+ it 'should assign version on create section' do
+ expect(section.version).to eq(1)
+ end
+
+ it 'should assign version on section' do
+ section.update_attributes!(title: 'Technology 2')
+ expect(section.version).to eq(2) # first track generated on creation
+ end
+
+ it 'should create a history track of version 2' do
+ section.update_attributes!(title: 'Technology 2')
+ expect(section.history_tracks.where(version: 2).first).not_to be_nil
+ end
+
+ it 'should assign modified fields' do
+ section.update_attributes!(title: 'Technology 2')
+ expect(section.history_tracks.where(version: 2).first.modified).to eq(
+ 't' => 'Technology 2'
+ )
+ end
+
+ it 'should assign original fields' do
+ section.update_attributes!(title: 'Technology 2')
+ expect(section.history_tracks.where(version: 2).first.original).to eq(
+ '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
+ expect(section.title).to eq('Technology')
+ end
+
+ it 'should assign modifier' do
+ section.update_attributes!(title: 'Business', modifier: another_user)
+ expect(post.history_tracks.last.modifier.id).to eq(another_user.id)
+ 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
+ expect(post.comments.first.title).to eq('test')
+ end
+
+ it 'should be possible to re-create destroyed embedded from parent' do
+ comment.destroy
+ post.history_tracks.last.undo!(user)
+ post.reload
+ expect(post.comments.first.title).to eq('test')
+ end
+
+ it 'should be possible to destroy after re-create embedded from parent' do
+ comment.destroy
+ post.history_tracks[-1].undo!(user)
+ post.history_tracks[-1].undo!(user)
+ post.reload
+ expect(post.comments.count).to eq(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', modifier: user)
+ track = post.history_tracks[2]
+ expect(post.reload.comments.count).to eq 2
+ track.undo!(user)
+ expect(post.reload.comments.count).to eq 1
+ track.redo!(user)
+ expect(post.reload.comments.count).to eq 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', updated_by: user) }
+
+ 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'])
+ expect(post.tags.last.title).to eq('baz')
+ end
+
+ it 'should be possible to destroy through parent model using canoncial _destroy macro' do
+ tag_foo
+ tag_bar # initialize
+ expect(post.tags.count).to eq(2)
+ update_hash = { 'post' => { 'tags_attributes' => { '1234' => { 'id' => tag_bar.id, 'title' => 'baz', '_destroy' => 'true' } } } }
+ post.update_attributes!(update_hash['post'])
+ expect(post.tags.count).to eq(1)
+ expect(post.history_tracks.to_a.last.action).to eq('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'
+ expect(tag_foo.history_tracks.last.association_chain.last['name']).to eq('tags')
+ expect { tag_foo.history_tracks.last.trackable }.not_to raise_error
+ end
+ end
+
+ describe 'non-embedded' do
+ it 'should undo changes' do
+ post.update_attributes!(title: 'Test2')
+ post.history_tracks.where(version: 2).last.undo!(user)
+ post.reload
+ expect(post.title).to eq('Test')
+ end
+
+ it 'should undo destruction' do
+ post.destroy
+ post.history_tracks.where(version: 2).last.undo!(user)
+ expect(Post.find(post.id).title).to eq('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
+ expect(post.history_tracks.count).to eq(4)
+ 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: 2).last.undo!(user)
+ post.reload
+ expect(post.history_tracks.where(version: 2).last.modifier.id).to eq(user.id)
+ 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
+
+ expect(post.title).to eq(post2.title)
+ end
+
+ it 'should be destroyed after undo and redo' do
+ post.destroy
+ track = post.history_tracks.where(version: 2).last
+ track.undo!(user)
+ track.redo!(user)
+ expect(Post.where(_id: post.id).first).to 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
+ expect(comment.title).to eq('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
+ expect(comment.history_tracks.count).to eq(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
+ expect(comment.history_tracks.where(version: 3).first.modifier.id).to eq(user.id)
+ 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
+ expect(comment.title).to eq('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
+ { 'undo' => [nil], 'undo!' => [nil, :reload] }.each do |test_method, methods|
+ methods.each do |method|
+ context (method || 'instance').to_s do
+ it 'recognizes :from, :to options' do
+ comment.send test_method, user, from: 4, to: 2
+ comment.send(method) if method
+ expect(comment.title).to eq('test')
+ end
+
+ it 'recognizes parameter as version number' do
+ comment.send test_method, user, 3
+ comment.send(method) if method
+ expect(comment.title).to eq('Test2')
+ end
+
+ it 'should undo last version when no parameter is specified' do
+ comment.send test_method, user
+ comment.send(method) if method
+ expect(comment.title).to eq('Test3')
+ end
+
+ it 'recognizes :last options' do
+ comment.send test_method, user, last: 2
+ comment.send(method) if method
+ expect(comment.title).to eq('Test2')
+ end
+
+ if Mongoid::Compatibility::Version.mongoid3?
+ 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.send test_method, user
+ comment.send(method) if method
+ expect(comment.title).to eq('Test3')
+ end
+
+ it 'recognizes :last options on model with protected attributes' do
+ comment.send test_method, user, last: 2
+ comment.send(method) if method
+ expect(comment.title).to eq('Test2')
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe 'redo' do
+ [nil, :reload].each do |method|
+ context (method || 'instance').to_s 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.send(method) if method
+ expect(comment.title).to eq('Test4')
+ end
+
+ it 'should recognize parameter as version number' do
+ comment.redo! user, 2
+ comment.send(method) if method
+ expect(comment.title).to eq('Test2')
+ end
+
+ it 'should redo last version when no parameter is specified' do
+ comment.redo! user
+ comment.send(method) if method
+ expect(comment.title).to eq('Test5')
+ end
+
+ it 'should recognize :last options' do
+ comment.redo! user, last: 1
+ comment.send(method) if method
+ expect(comment.title).to eq('Test5')
+ end
+
+ if Mongoid::Compatibility::Version.mongoid3?
+ 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.send(method) if method
+ expect(comment.title).to eq('Test2')
+ end
+
+ it 'should recognize :from, :to options' do
+ comment.redo! user, from: 2, to: 4
+ comment.send(method) if method
+ expect(comment.title).to eq('Test4')
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe 'embedded with a polymorphic trackable' do
+ let(:foo) { Foo.new(title: 'a title', body: 'a body', modifier: user) }
+ 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 }
+ expect(foo.history_tracks.first.association_chain).to eq([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
+ expect(tag.history_tracks.first.trackable_parent_class).to eq(Tag)
+ end
+ it 'should return the parent class even if the trackable is deleted' do
+ tracker = tag.history_tracks.first
+ tag.destroy
+ expect(tracker.trackable_parent_class).to eq(Tag)
+ end
+ end
+ context 'an embedded model' do
+ it 'should return the trackable parent class' do
+ comment.update_attributes!(title: 'Foo')
+ expect(comment.history_tracks.first.trackable_parent_class).to eq(Post)
+ end
+ it 'should return the parent class even if the trackable is deleted' do
+ tracker = comment.history_tracks.first
+ comment.destroy
+ expect(tracker.trackable_parent_class).to eq(Post)
+ end
+ end
+ end
+
+ describe 'when default scope is present' do
+ before :each do
+ class Post
+ default_scope -> { where(title: nil) }
+ end
+ class Comment
+ default_scope -> { where(title: nil) }
+ end
+ class User
+ default_scope -> { where(name: nil) }
+ end
+ class Tag
+ default_scope -> { where(title: nil) }
+ end
+ end
+
+ describe 'post' do
+ it 'should correctly undo and redo' do
+ post.update_attributes!(title: 'a new title')
+ track = post.history_tracks.last
+ track.undo! user
+ expect(post.reload.title).to eq('Test')
+ track.redo! user
+ expect(post.reload.title).to eq('a new title')
+ end
+
+ it 'should stay the same after undo and redo' do
+ post.update_attributes!(title: 'testing')
+ track = post.history_tracks.last
+ track.undo! user
+ track.redo! user
+ expect(post.reload.title).to eq('testing')
+ end
+ end
+ describe 'comment' do
+ it 'should correctly undo and redo' do
+ comment.update_attributes!(title: 'a new title')
+ track = comment.history_tracks.last
+ track.undo! user
+ expect(comment.reload.title).to eq('test')
+ track.redo! user
+ expect(comment.reload.title).to eq('a new title')
+ end
+
+ it 'should stay the same after undo and redo' do
+ comment.update_attributes!(title: 'testing')
+ track = comment.history_tracks.last
+ track.undo! user
+ track.redo! user
+ expect(comment.reload.title).to eq('testing')
+ end
+ end
+ describe 'user' do
+ it 'should correctly undo and redo' do
+ user.update_attributes!(name: 'a new name')
+ track = user.history_tracks.last
+ track.undo! user
+ expect(user.reload.name).to eq('Aaron')
+ track.redo! user
+ expect(user.reload.name).to eq('a new name')
+ end
+
+ it 'should stay the same after undo and redo' do
+ user.update_attributes!(name: 'testing')
+ track = user.history_tracks.last
+ track.undo! user
+ track.redo! user
+ expect(user.reload.name).to eq('testing')
+ end
+ end
+ describe 'tag' do
+ it 'should correctly undo and redo' do
+ tag.update_attributes!(title: 'a new title')
+ track = tag.history_tracks.last
+ track.undo! user
+ expect(tag.reload.title).to eq('test')
+ track.redo! user
+ expect(tag.reload.title).to eq('a new title')
+ end
+
+ it 'should stay the same after undo and redo' do
+ tag.update_attributes!(title: 'testing')
+ track = tag.history_tracks.last
+ track.undo! user
+ track.redo! user
+ expect(tag.reload.title).to eq('testing')
+ end
+ end
+ end
+
+ describe 'overriden changes_method with additional fields' do
+ before :each do
+ class OverriddenChangesMethod
+ include Mongoid::Document
+ include Mongoid::History::Trackable
+
+ track_history on: [:foo], changes_method: :my_changes
+
+ def my_changes
+ { foo: %w[bar baz] }
+ end
+ end
+ end
+
+ after :each do
+ Object.send(:remove_const, :OverriddenChangesMethod)
+ end
+
+ it 'should add foo to the changes history' do
+ o = OverriddenChangesMethod.create(modifier: user)
+ o.save!
+ track = o.history_tracks.last
+ expect(track.modified).to eq('foo' => 'baz')
+ expect(track.original).to eq('foo' => 'bar')
+ 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, modifier_field_optional: true
+ end
+ end
+
+ after :each do
+ Object.send(:remove_const, :Sausage)
+ end
+
+ it 'should correctly undo and redo' do
+ pending unless Sausage.respond_to?(:localized_fields)
+
+ sausage = Sausage.create!(flavour_translations: { 'en' => 'Apple', 'nl' => 'Appel' }, modifier: user)
+ sausage.update_attributes!(flavour: 'Guinness')
+
+ track = sausage.history_tracks.last
+
+ track.undo! user
+ expect(sausage.reload.flavour).to eq('Apple')
+
+ track.redo! user
+ expect(sausage.reload.flavour).to eq('Guinness')
+
+ sausage.destroy
+ expect(sausage.history_tracks.last.action).to eq('destroy')
+ sausage.history_tracks.last.undo! user
+ expect(sausage.reload.flavour).to eq('Guinness')
+ end
+ end
+
+ describe 'changing collection' do
+ before :each do
+ class Fish
+ include Mongoid::Document
+ include Mongoid::History::Trackable
+
+ track_history on: [:species], modifier_field_optional: true
+ store_in collection: :animals
+
+ field :species
+ end
+ end
+
+ after :each do
+ Object.send(:remove_const, :Fish)
+ end
+
+ it 'should track history' do
+ Fish.new.save!
+ end
+ end
+ end
+end