mongoid-audit 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,159 @@
1
+ module Mongoid::Audit
2
+ module Tracker
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ include Mongoid::Document
7
+ include Mongoid::Timestamps
8
+ attr_writer :trackable
9
+
10
+ field :association_chain, :type => Array, :default => []
11
+ field :modified, :type => Hash
12
+ field :original, :type => Hash
13
+ field :version, :type => Integer
14
+ field :action, :type => String
15
+ field :scope, :type => String
16
+ belongs_to :modifier, :class_name => Mongoid::Audit.modifier_class_name
17
+
18
+ Mongoid::Audit.tracker_class_name = self.name.tableize.singularize.to_sym
19
+
20
+ # install model observer and action controller filter
21
+ Mongoid::Audit::Sweeper.send(:observe, Mongoid::Audit.tracker_class_name)
22
+ if defined?(ActionController) and defined?(ActionController::Base)
23
+ ActionController::Base.class_eval do
24
+ before_filter { |controller| Mongoid::Audit::Sweeper.instance.before(controller) }
25
+ after_filter { |controller| Mongoid::Audit::Sweeper.instance.after(controller) }
26
+ end
27
+ end
28
+ end
29
+
30
+ def undo!(modifier)
31
+ if action.to_sym == :destroy
32
+ re_create
33
+ elsif action.to_sym == :create
34
+ re_destroy
35
+ else
36
+ trackable.update_attributes!(undo_attr(modifier))
37
+ end
38
+ end
39
+
40
+ def redo!(modifier)
41
+ if action.to_sym == :destroy
42
+ re_destroy
43
+ elsif action.to_sym == :create
44
+ re_create
45
+ else
46
+ trackable.update_attributes!(redo_attr(modifier))
47
+ end
48
+ end
49
+
50
+ def undo_attr(modifier)
51
+ undo_hash = affected.easy_unmerge(modified)
52
+ undo_hash.easy_merge!(original)
53
+ modifier_field = trackable.history_trackable_options[:modifier_field]
54
+ undo_hash[modifier_field] = modifier
55
+ undo_hash
56
+ end
57
+
58
+ def redo_attr(modifier)
59
+ redo_hash = affected.easy_unmerge(original)
60
+ redo_hash.easy_merge!(modified)
61
+ modifier_field = trackable.history_trackable_options[:modifier_field]
62
+ redo_hash[modifier_field] = modifier
63
+ redo_hash
64
+ end
65
+
66
+ def trackable_root
67
+ @trackable_root ||= trackable_parents_and_trackable.first
68
+ end
69
+
70
+ def trackable
71
+ @trackable ||= trackable_parents_and_trackable.last
72
+ end
73
+
74
+ def trackable_parents
75
+ @trackable_parents ||= trackable_parents_and_trackable[0, -1]
76
+ end
77
+
78
+ def trackable_parent
79
+ @trackable_parent ||= trackable_parents_and_trackable[-2]
80
+ end
81
+
82
+
83
+ def affected
84
+ @affected ||= (modified.keys | original.keys).inject({}){ |h,k| h[k] =
85
+ trackable ? trackable.attributes[k] : modified[k]; h}
86
+ end
87
+
88
+ private
89
+
90
+ def re_create
91
+ association_chain.length > 1 ? create_on_parent : create_standalone
92
+ end
93
+
94
+ def re_destroy
95
+ trackable.destroy
96
+ end
97
+
98
+ def create_standalone
99
+ class_name = association_chain.first["name"]
100
+ restored = class_name.constantize.new(modified)
101
+ restored.id = modified["_id"]
102
+ restored.save!
103
+ end
104
+
105
+ def create_on_parent
106
+ name = association_chain.last["name"]
107
+ if embeds_one?(trackable_parent, name)
108
+ trackable_parent.send("create_#{name}!", modified)
109
+ elsif embeds_many?(trackable_parent, name)
110
+ trackable_parent.send(name).create!(modified)
111
+ else
112
+ raise "This should never happen. Please report bug!"
113
+ end
114
+ end
115
+
116
+ def trackable_parents_and_trackable
117
+ @trackable_parents_and_trackable ||= traverse_association_chain
118
+ end
119
+
120
+ def relation_of(doc, name)
121
+ meta = doc.reflect_on_association(name)
122
+ meta ? meta.relation : nil
123
+ end
124
+
125
+ def embeds_one?(doc, name)
126
+ relation_of(doc, name) == Mongoid::Relations::Embedded::One
127
+ end
128
+
129
+ def embeds_many?(doc, name)
130
+ relation_of(doc, name) == Mongoid::Relations::Embedded::Many
131
+ end
132
+
133
+ def traverse_association_chain
134
+ chain = association_chain.dup
135
+ doc = nil
136
+ documents = []
137
+
138
+ begin
139
+ node = chain.shift
140
+ name = node['name']
141
+
142
+ doc = if doc.nil?
143
+ # root association. First element of the association chain
144
+ klass = name.classify.constantize
145
+ klass.where(:_id => node['id']).first
146
+ elsif embeds_one?(doc, name)
147
+ doc.send(name)
148
+ elsif embeds_many?(doc, name)
149
+ doc.send(name).where(:_id => node['id']).first
150
+ else
151
+ raise "This should never happen. Please report bug."
152
+ end
153
+ documents << doc
154
+ end while( !chain.empty? )
155
+ documents
156
+ end
157
+
158
+ end
159
+ end
@@ -0,0 +1,5 @@
1
+ module Mongoid
2
+ module Audit
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mongoid-audit/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "mongoid-audit"
8
+ gem.version = Mongoid::Audit::VERSION
9
+ gem.authors = ["Gleb Tv"]
10
+ gem.email = ["glebtv@gmail.com"]
11
+ gem.description = %q{Mongoid Audit}
12
+ gem.summary = %q{Easily track model change history and mantain audit log with mongoid}
13
+ gem.homepage = "https://github.com/rs-pro/mongoid-audit"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_runtime_dependency('easy_diff', ">= 0")
21
+ gem.add_runtime_dependency('mongoid', ">= 3.0.4")
22
+
23
+ gem.add_development_dependency('rspec', ["~> 2.12.0"])
24
+ gem.add_development_dependency('bundler', ['>= 1.0.0'])
25
+ gem.add_development_dependency('database_cleaner', [">= 0.8.0"])
26
+ gem.add_development_dependency('activesupport', '~> 3.2.11')
27
+ end
@@ -0,0 +1,553 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Mongoid::Audit do
4
+ before :all do
5
+ class HistoryTracker
6
+ include Mongoid::Audit::Tracker
7
+ end
8
+
9
+ class Post
10
+ include Mongoid::Document
11
+ include Mongoid::Timestamps
12
+ include Mongoid::Audit::Trackable
13
+
14
+ field :title
15
+ field :body
16
+ field :rating
17
+
18
+ embeds_many :comments
19
+ embeds_one :section
20
+ embeds_many :tags
21
+
22
+ accepts_nested_attributes_for :tags, :allow_destroy => true
23
+
24
+ track_history :on => [:title, :body], :track_destroy => true
25
+ end
26
+
27
+ class Comment
28
+ include Mongoid::Document
29
+ include Mongoid::Timestamps
30
+ include Mongoid::Audit::Trackable
31
+
32
+ field :title
33
+ field :body
34
+ embedded_in :post
35
+ track_history :on => [:title, :body], :scope => :post, :track_create => true, :track_destroy => true
36
+ end
37
+
38
+ class Section
39
+ include Mongoid::Document
40
+ include Mongoid::Timestamps
41
+ include Mongoid::Audit::Trackable
42
+
43
+ field :title
44
+ embedded_in :post
45
+ track_history :on => [:title], :scope => :post, :track_create => true, :track_destroy => true
46
+ end
47
+
48
+ class User
49
+ include Mongoid::Document
50
+ include Mongoid::Timestamps
51
+ include Mongoid::Audit::Trackable
52
+
53
+ field :email
54
+ field :name
55
+ track_history :except => [:email]
56
+ end
57
+
58
+ class Tag
59
+ include Mongoid::Document
60
+ include Mongoid::Timestamps
61
+ include Mongoid::Audit::Trackable
62
+
63
+ belongs_to :updated_by, :class_name => "User"
64
+
65
+ field :title
66
+ track_history :on => [:title], :scope => :post, :track_create => true, :track_destroy => true, :modifier_field => :updated_by
67
+ end
68
+ end
69
+
70
+ before :each do
71
+ @user = User.create(:name => "Aaron", :email => "aaron@randomemail.com")
72
+ @another_user = User.create(:name => "Another Guy", :email => "anotherguy@randomemail.com")
73
+ @post = Post.create(:title => "Test", :body => "Post", :modifier => @user, :views => 100)
74
+ @comment = @post.comments.create(:title => "test", :body => "comment", :modifier => @user)
75
+ end
76
+
77
+ describe "track" do
78
+ describe "on creation" do
79
+ it "should have one history track in comment" do
80
+ @comment.history_tracks.count.should == 1
81
+ end
82
+
83
+ it "should assign title and body on modified" do
84
+ @comment.history_tracks.first.modified.should == {'title' => "test", 'body' => "comment"}
85
+ end
86
+
87
+ it "should not assign title and body on original" do
88
+ @comment.history_tracks.first.original.should == {}
89
+ end
90
+
91
+ it "should assign modifier" do
92
+ @comment.history_tracks.first.modifier.should == @user
93
+ end
94
+
95
+ it "should assign version" do
96
+ @comment.history_tracks.first.version.should == 1
97
+ end
98
+
99
+ it "should assign scope" do
100
+ @comment.history_tracks.first.scope.should == "post"
101
+ end
102
+
103
+ it "should assign method" do
104
+ @comment.history_tracks.first.action.should == "create"
105
+ end
106
+
107
+ it "should assign association_chain" do
108
+ expected = [
109
+ {'id' => @post.id, 'name' => "Post"},
110
+ {'id' => @comment.id, 'name' => "comments"}
111
+ ]
112
+ @comment.history_tracks.first.association_chain.should == expected
113
+ end
114
+ end
115
+
116
+ describe "on destruction" do
117
+ it "should have two history track records in post" do
118
+ lambda {
119
+ @post.destroy
120
+ }.should change(HistoryTracker, :count).by(1)
121
+ end
122
+
123
+ it "should assign destroy on track record" do
124
+ @post.destroy
125
+ @post.history_tracks.last.action.should == "destroy"
126
+ end
127
+
128
+ it "should return affected attributes from track record" do
129
+ @post.destroy
130
+ @post.history_tracks.last.affected["title"].should == "Test"
131
+ end
132
+ end
133
+
134
+ describe "on update non-embedded" do
135
+ it "should create a history track if changed attributes match tracked attributes" do
136
+ lambda {
137
+ @post.update_attributes(:title => "Another Test")
138
+ }.should change(HistoryTracker, :count).by(1)
139
+ end
140
+
141
+ it "should not create a history track if changed attributes do not match tracked attributes" do
142
+ lambda {
143
+ @post.update_attributes(:rating => "untracked")
144
+ }.should change(HistoryTracker, :count).by(0)
145
+ end
146
+
147
+ it "should assign modified fields" do
148
+ @post.update_attributes(:title => "Another Test")
149
+ @post.history_tracks.last.modified.should == {
150
+ "title" => "Another Test"
151
+ }
152
+ end
153
+
154
+ it "should assign method field" do
155
+ @post.update_attributes(:title => "Another Test")
156
+ @post.history_tracks.last.action.should == "update"
157
+ end
158
+
159
+ it "should assign original fields" do
160
+ @post.update_attributes(:title => "Another Test")
161
+ @post.history_tracks.last.original.should == {
162
+ "title" => "Test"
163
+ }
164
+ end
165
+
166
+ it "should assign modifier" do
167
+ @post.update_attributes(:title => "Another Test")
168
+ @post.history_tracks.first.modifier.should == @user
169
+ end
170
+
171
+ it "should assign version on history tracks" do
172
+ @post.update_attributes(:title => "Another Test")
173
+ @post.history_tracks.first.version.should == 1
174
+ end
175
+
176
+ it "should assign version on post" do
177
+ @post.update_attributes(:title => "Another Test")
178
+ @post.version.should == 1
179
+ end
180
+
181
+ it "should assign scope" do
182
+ @post.update_attributes(:title => "Another Test")
183
+ @post.history_tracks.first.scope.should == "post"
184
+ end
185
+
186
+ it "should assign association_chain" do
187
+ @post.update_attributes(:title => "Another Test")
188
+ @post.history_tracks.last.association_chain.should == [{'id' => @post.id, 'name' => "Post"}]
189
+ end
190
+
191
+ it "should exclude defined options" do
192
+ @user.update_attributes(:name => "Aaron2", :email => "aaronsnewemail@randomemail.com")
193
+ @user.history_tracks.first.modified.keys.should include "name"
194
+ @user.history_tracks.first.modified.keys.should_not include "email"
195
+ end
196
+ end
197
+
198
+ describe "on update non-embedded twice" do
199
+ it "should assign version on post" do
200
+ @post.update_attributes(:title => "Test2")
201
+ @post.update_attributes(:title => "Test3")
202
+ @post.version.should == 2
203
+ end
204
+
205
+ it "should create a history track if changed attributes match tracked attributes" do
206
+ lambda {
207
+ @post.update_attributes(:title => "Test2")
208
+ @post.update_attributes(:title => "Test3")
209
+ }.should change(HistoryTracker, :count).by(2)
210
+ end
211
+
212
+ it "should create a history track of version 2" do
213
+ @post.update_attributes(:title => "Test2")
214
+ @post.update_attributes(:title => "Test3")
215
+ @post.history_tracks.where(:version => 2).first.should_not be_nil
216
+ end
217
+
218
+ it "should assign modified fields" do
219
+ @post.update_attributes(:title => "Test2")
220
+ @post.update_attributes(:title => "Test3")
221
+ @post.history_tracks.where(:version => 2).first.modified.should == {
222
+ "title" => "Test3"
223
+ }
224
+ end
225
+
226
+ it "should assign original fields" do
227
+ @post.update_attributes(:title => "Test2")
228
+ @post.update_attributes(:title => "Test3")
229
+ @post.history_tracks.where(:version => 2).first.original.should == {
230
+ "title" => "Test2"
231
+ }
232
+ end
233
+
234
+
235
+ it "should assign modifier" do
236
+ @post.update_attributes(:title => "Another Test", :modifier => @another_user)
237
+ @post.history_tracks.last.modifier.should == @another_user
238
+ end
239
+ end
240
+
241
+ describe "on update embedded 1..N (embeds_many)" do
242
+ it "should assign version on comment" do
243
+ @comment.update_attributes(:title => "Test2")
244
+ @comment.version.should == 2 # first track generated on creation
245
+ end
246
+
247
+ it "should create a history track of version 2" do
248
+ @comment.update_attributes(:title => "Test2")
249
+ @comment.history_tracks.where(:version => 2).first.should_not be_nil
250
+ end
251
+
252
+ it "should assign modified fields" do
253
+ @comment.update_attributes(:title => "Test2")
254
+ @comment.history_tracks.where(:version => 2).first.modified.should == {
255
+ "title" => "Test2"
256
+ }
257
+ end
258
+
259
+ it "should assign original fields" do
260
+ @comment.update_attributes(:title => "Test2")
261
+ @comment.history_tracks.where(:version => 2).first.original.should == {
262
+ "title" => "test"
263
+ }
264
+ end
265
+
266
+ it "should be possible to undo from parent" do
267
+ @comment.update_attributes(:title => "Test 2")
268
+ @post.history_tracks.last.undo!(@user)
269
+ @comment.reload
270
+ @comment.title.should == "test"
271
+ end
272
+
273
+ it "should assign modifier" do
274
+ @post.update_attributes(:title => "Another Test", :modifier => @another_user)
275
+ @post.history_tracks.last.modifier.should == @another_user
276
+ end
277
+ end
278
+
279
+ describe "on update embedded 1..1 (embeds_one)" do
280
+ before(:each) do
281
+ @section = Section.new(:title => 'Technology')
282
+ @post.section = @section
283
+ @post.save!
284
+ @post.reload
285
+ @section = @post.section
286
+ end
287
+
288
+ it "should assign version on create section" do
289
+ @section.version.should == 1
290
+ end
291
+
292
+ it "should assign version on section" do
293
+ @section.update_attributes(:title => 'Technology 2')
294
+ @section.version.should == 2 # first track generated on creation
295
+ end
296
+
297
+ it "should create a history track of version 2" do
298
+ @section.update_attributes(:title => 'Technology 2')
299
+ @section.history_tracks.where(:version => 2).first.should_not be_nil
300
+ end
301
+
302
+ it "should assign modified fields" do
303
+ @section.update_attributes(:title => 'Technology 2')
304
+ @section.history_tracks.where(:version => 2).first.modified.should == {
305
+ "title" => "Technology 2"
306
+ }
307
+ end
308
+
309
+ it "should assign original fields" do
310
+ @section.update_attributes(:title => 'Technology 2')
311
+ @section.history_tracks.where(:version => 2).first.original.should == {
312
+ "title" => "Technology"
313
+ }
314
+ end
315
+
316
+ it "should be possible to undo from parent" do
317
+ @section.update_attributes(:title => 'Technology 2')
318
+ @post.history_tracks.last.undo!(@user)
319
+ @section.reload
320
+ @section.title.should == "Technology"
321
+ end
322
+
323
+ it "should assign modifier" do
324
+ @section.update_attributes(:title => "Business", :modifier => @another_user)
325
+ @post.history_tracks.last.modifier.should == @another_user
326
+ end
327
+ end
328
+
329
+ describe "on destroy embedded" do
330
+ it "should be possible to re-create destroyed embedded" do
331
+ @comment.destroy
332
+ @comment.history_tracks.last.undo!(@user)
333
+ @post.reload
334
+ @post.comments.first.title.should == "test"
335
+ end
336
+
337
+ it "should be possible to re-create destroyed embedded from parent" do
338
+ @comment.destroy
339
+ @post.history_tracks.last.undo!(@user)
340
+ @post.reload
341
+ @post.comments.first.title.should == "test"
342
+ end
343
+
344
+ it "should be possible to destroy after re-create embedded from parent" do
345
+ @comment.destroy
346
+ @post.history_tracks.last.undo!(@user)
347
+ @post.history_tracks.last.undo!(@user)
348
+ @post.reload
349
+ @post.comments.count.should == 0
350
+ end
351
+
352
+ it "should be possible to create with redo after undo create embedded from parent" do
353
+ @post.comments.create!(:title => "The second one")
354
+ @track = @post.history_tracks.last
355
+ @track.undo!(@user)
356
+ @track.redo!(@user)
357
+ @post.reload
358
+ @post.comments.count.should == 2
359
+ end
360
+ end
361
+
362
+ describe "embedded with cascading callbacks" do
363
+ before(:each) do
364
+ Mongoid.instantiate_observers
365
+ Thread.current[:mongoid_history_sweeper_controller] = self
366
+ self.stub!(:current_user).and_return @user
367
+ @tag_foo = @post.tags.create(:title => "foo", :updated_by => @user)
368
+ @tag_bar = @post.tags.create(:title => "bar")
369
+ end
370
+
371
+ after(:each) do
372
+ Thread.current[:mongoid_history_sweeper_controller] = nil
373
+ end
374
+
375
+ it "should have cascaded the creation callbacks and set timestamps" do
376
+ @tag_foo.created_at.should_not be_nil
377
+ @tag_foo.updated_at.should_not be_nil
378
+ end
379
+
380
+ it "should allow an update through the parent model" do
381
+ update_hash = { "post" => { "tags_attributes" => { "1234" => { "id" => @tag_bar.id, "title" => "baz" } } } }
382
+ @post.update_attributes(update_hash["post"])
383
+ @post.tags.last.title.should == "baz"
384
+ end
385
+
386
+ it "should be possible to destroy through parent model using canoncial _destroy macro" do
387
+ @post.tags.count.should == 2
388
+ update_hash = { "post" => { "tags_attributes" => { "1234" => { "id" => @tag_bar.id, "title" => "baz", "_destroy" => "true"} } } }
389
+ @post.update_attributes(update_hash["post"])
390
+ @post.tags.count.should == 1
391
+ @post.history_tracks.last.action.should == "destroy"
392
+ end
393
+
394
+ it "should write relationship name for association_chain hiearchy instead of class name when using _destroy macro" do
395
+ update_hash = {"tags_attributes" => { "1234" => { "id" => @tag_foo.id, "_destroy" => "1"} } }
396
+ @post.update_attributes(update_hash)
397
+
398
+ # historically this would have evaluated to 'Tags' and an error would be thrown
399
+ # on any call that walked up the association_chain, e.g. 'trackable'
400
+ @tag_foo.history_tracks.last.association_chain.last["name"].should == "tags"
401
+ lambda{ @tag_foo.history_tracks.last.trackable }.should_not raise_error
402
+ end
403
+
404
+ it "should save modifier" do
405
+ @tag_foo.history_tracks.last.modifier.should eq @user
406
+ @tag_bar.history_tracks.last.modifier.should eq @user
407
+ end
408
+ end
409
+
410
+ describe "non-embedded" do
411
+ it "should undo changes" do
412
+ @post.update_attributes(:title => "Test2")
413
+ @post.history_tracks.where(:version => 1).last.undo!(@user)
414
+ @post.reload
415
+ @post.title.should == "Test"
416
+ end
417
+
418
+ it "should undo destruction" do
419
+ @post.destroy
420
+ @post.history_tracks.where(:version => 1).last.undo!(@user)
421
+ Post.find(@post.id).title.should == "Test"
422
+ end
423
+
424
+ it "should create a new history track after undo" do
425
+ @post.update_attributes(:title => "Test2")
426
+ @post.history_tracks.last.undo!(@user)
427
+ @post.reload
428
+ @post.history_tracks.count.should == 3
429
+ end
430
+
431
+ it "should assign @user as the modifier of the newly created history track" do
432
+ @post.update_attributes(:title => "Test2")
433
+ @post.history_tracks.where(:version => 1).last.undo!(@user)
434
+ @post.reload
435
+ @post.history_tracks.where(:version => 2).last.modifier.should == @user
436
+ end
437
+
438
+ it "should stay the same after undo and redo" do
439
+ @post.update_attributes(:title => "Test2")
440
+ @track = @post.history_tracks.last
441
+ @track.undo!(@user)
442
+ @track.redo!(@user)
443
+ @post2 = Post.where(:_id => @post.id).first
444
+
445
+ @post.title.should == @post2.title
446
+ end
447
+
448
+ it "should be destroyed after undo and redo" do
449
+ @post.destroy
450
+ @track = @post.history_tracks.where(:version => 1).last
451
+ @track.undo!(@user)
452
+ @track.redo!(@user)
453
+ Post.where(:_id => @post.id).first.should == nil
454
+ end
455
+ end
456
+
457
+ describe "embedded" do
458
+ it "should undo changes" do
459
+ @comment.update_attributes(:title => "Test2")
460
+ @comment.history_tracks.where(:version => 2).first.undo!(@user)
461
+ # reloading an embedded document === KAMIKAZE
462
+ # at least for the current release of mongoid...
463
+ @post.reload
464
+ @comment = @post.comments.first
465
+ @comment.title.should == "test"
466
+ end
467
+
468
+ it "should create a new history track after undo" do
469
+ @comment.update_attributes(:title => "Test2")
470
+ @comment.history_tracks.where(:version => 2).first.undo!(@user)
471
+ @post.reload
472
+ @comment = @post.comments.first
473
+ @comment.history_tracks.count.should == 3
474
+ end
475
+
476
+ it "should assign @user as the modifier of the newly created history track" do
477
+ @comment.update_attributes(:title => "Test2")
478
+ @comment.history_tracks.where(:version => 2).first.undo!(@user)
479
+ @post.reload
480
+ @comment = @post.comments.first
481
+ @comment.history_tracks.where(:version => 3).first.modifier.should == @user
482
+ end
483
+
484
+ it "should stay the same after undo and redo" do
485
+ @comment.update_attributes(:title => "Test2")
486
+ @track = @comment.history_tracks.where(:version => 2).first
487
+ @track.undo!(@user)
488
+ @track.redo!(@user)
489
+ @post2 = Post.where(:_id => @post.id).first
490
+ @comment2 = @post2.comments.first
491
+
492
+ @comment.title.should == @comment2.title
493
+ end
494
+ end
495
+
496
+ describe "trackables" do
497
+ before :each do
498
+ @comment.update_attributes(:title => "Test2") # version == 2
499
+ @comment.update_attributes(:title => "Test3") # version == 3
500
+ @comment.update_attributes(:title => "Test4") # version == 4
501
+ end
502
+
503
+ describe "undo" do
504
+ it "should recognize :from, :to options" do
505
+ @comment.undo! @user, :from => 4, :to => 2
506
+ @comment.title.should == "test"
507
+ end
508
+
509
+ it "should recognize parameter as version number" do
510
+ @comment.undo! @user, 3
511
+ @comment.title.should == "Test2"
512
+ end
513
+
514
+ it "should undo last version when no parameter is specified" do
515
+ @comment.undo! @user
516
+ @comment.title.should == "Test3"
517
+ end
518
+
519
+ it "should recognize :last options" do
520
+ @comment.undo! @user, :last => 2
521
+ @comment.title.should == "Test2"
522
+ end
523
+ end
524
+
525
+ describe "redo" do
526
+ before :each do
527
+ @comment.update_attributes(:title => "Test5")
528
+ end
529
+
530
+ it "should recognize :from, :to options" do
531
+ @comment.redo! @user, :from => 2, :to => 4
532
+ @comment.title.should == "Test4"
533
+ end
534
+
535
+ it "should recognize parameter as version number" do
536
+ @comment.redo! @user, 2
537
+ @comment.title.should == "Test2"
538
+ end
539
+
540
+ it "should redo last version when no parameter is specified" do
541
+ @comment.redo! @user
542
+ @comment.title.should == "Test5"
543
+ end
544
+
545
+ it "should recognize :last options" do
546
+ @comment.redo! @user, :last => 1
547
+ @comment.title.should == "Test5"
548
+ end
549
+
550
+ end
551
+ end
552
+ end
553
+ end