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