mongoid-history 0.8.3 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.coveralls.yml +1 -1
  3. data/.document +5 -5
  4. data/.github/workflows/test.yml +72 -0
  5. data/.gitignore +46 -46
  6. data/.rspec +2 -2
  7. data/.rubocop.yml +6 -6
  8. data/.rubocop_todo.yml +99 -99
  9. data/CHANGELOG.md +173 -163
  10. data/CONTRIBUTING.md +117 -118
  11. data/Dangerfile +1 -1
  12. data/Gemfile +49 -40
  13. data/LICENSE.txt +20 -20
  14. data/README.md +609 -608
  15. data/RELEASING.md +66 -67
  16. data/Rakefile +24 -24
  17. data/UPGRADING.md +53 -53
  18. data/lib/mongoid/history/attributes/base.rb +72 -72
  19. data/lib/mongoid/history/attributes/create.rb +45 -45
  20. data/lib/mongoid/history/attributes/destroy.rb +34 -34
  21. data/lib/mongoid/history/attributes/update.rb +104 -104
  22. data/lib/mongoid/history/options.rb +177 -177
  23. data/lib/mongoid/history/trackable.rb +588 -583
  24. data/lib/mongoid/history/tracker.rb +247 -247
  25. data/lib/mongoid/history/version.rb +5 -5
  26. data/lib/mongoid/history.rb +77 -77
  27. data/lib/mongoid-history.rb +1 -1
  28. data/mongoid-history.gemspec +25 -25
  29. data/perf/benchmark_modified_attributes_for_create.rb +65 -65
  30. data/perf/gc_suite.rb +21 -21
  31. data/spec/integration/embedded_in_polymorphic_spec.rb +112 -112
  32. data/spec/integration/integration_spec.rb +976 -976
  33. data/spec/integration/multi_relation_spec.rb +47 -47
  34. data/spec/integration/multiple_trackers_spec.rb +68 -68
  35. data/spec/integration/nested_embedded_documents_spec.rb +64 -64
  36. data/spec/integration/nested_embedded_documents_tracked_in_parent_spec.rb +124 -124
  37. data/spec/integration/nested_embedded_polymorphic_documents_spec.rb +115 -115
  38. data/spec/integration/subclasses_spec.rb +47 -47
  39. data/spec/integration/track_history_order_spec.rb +84 -84
  40. data/spec/integration/validation_failure_spec.rb +76 -76
  41. data/spec/spec_helper.rb +32 -30
  42. data/spec/support/error_helpers.rb +7 -0
  43. data/spec/support/mongoid.rb +11 -11
  44. data/spec/support/mongoid_history.rb +12 -12
  45. data/spec/unit/attributes/base_spec.rb +141 -141
  46. data/spec/unit/attributes/create_spec.rb +342 -342
  47. data/spec/unit/attributes/destroy_spec.rb +228 -228
  48. data/spec/unit/attributes/update_spec.rb +342 -342
  49. data/spec/unit/callback_options_spec.rb +165 -165
  50. data/spec/unit/embedded_methods_spec.rb +87 -87
  51. data/spec/unit/history_spec.rb +58 -58
  52. data/spec/unit/my_instance_methods_spec.rb +555 -555
  53. data/spec/unit/options_spec.rb +365 -365
  54. data/spec/unit/singleton_methods_spec.rb +406 -406
  55. data/spec/unit/store/default_store_spec.rb +11 -11
  56. data/spec/unit/store/request_store_spec.rb +13 -13
  57. data/spec/unit/trackable_spec.rb +1057 -987
  58. data/spec/unit/tracker_spec.rb +190 -190
  59. metadata +9 -7
  60. data/.travis.yml +0 -36
@@ -1,976 +1,976 @@
1
- require 'spec_helper'
2
-
3
- describe Mongoid::History do
4
- before :each do
5
- class Post
6
- include Mongoid::Document
7
- include Mongoid::Timestamps
8
- include Mongoid::History::Trackable
9
-
10
- field :title
11
- field :body
12
- field :rating
13
- field :views, type: Integer
14
-
15
- embeds_many :comments, store_as: :coms
16
- embeds_one :section, store_as: :sec
17
- embeds_many :tags, cascade_callbacks: true
18
-
19
- accepts_nested_attributes_for :tags, allow_destroy: true
20
-
21
- track_history on: %i[title body], track_destroy: true
22
- end
23
-
24
- class Comment
25
- include Mongoid::Document
26
- include Mongoid::Timestamps
27
- include Mongoid::History::Trackable
28
-
29
- field :t, as: :title
30
- field :body
31
- embedded_in :commentable, polymorphic: true
32
- # BUG: see https://github.com/mongoid/mongoid-history/issues/223, modifier_field_optional should not be necessary
33
- track_history on: %i[title body], scope: :post, track_create: true, track_destroy: true, modifier_field_optional: true
34
- end
35
-
36
- class Section
37
- include Mongoid::Document
38
- include Mongoid::Timestamps
39
- include Mongoid::History::Trackable
40
-
41
- field :t, as: :title
42
- embedded_in :post
43
- track_history on: [:title], scope: :post, track_create: true, track_destroy: true
44
- end
45
-
46
- class User
47
- include Mongoid::Document
48
- include Mongoid::Timestamps
49
- include Mongoid::History::Trackable
50
-
51
- field :n, as: :name
52
- field :em, as: :email
53
- field :phone
54
- field :address
55
- field :city
56
- field :country
57
- field :aliases, type: Array
58
- track_history except: %i[email updated_at], modifier_field_optional: true
59
- end
60
-
61
- class Tag
62
- include Mongoid::Document
63
- # include Mongoid::Timestamps (see: https://github.com/mongoid/mongoid/issues/3078)
64
- include Mongoid::History::Trackable
65
-
66
- belongs_to :updated_by, class_name: 'User'
67
-
68
- field :title
69
- track_history on: [:title], scope: :post, track_create: true, track_destroy: true, modifier_field: :updated_by
70
- end
71
-
72
- class Foo < Comment
73
- end
74
- end
75
-
76
- after :each do
77
- Object.send(:remove_const, :Post)
78
- Object.send(:remove_const, :Comment)
79
- Object.send(:remove_const, :Section)
80
- Object.send(:remove_const, :User)
81
- Object.send(:remove_const, :Tag)
82
- Object.send(:remove_const, :Foo)
83
- end
84
-
85
- let(:user) { User.create!(name: 'Aaron', email: 'aaron@randomemail.com', aliases: ['bob'], country: 'Canada', city: 'Toronto', address: '21 Jump Street') }
86
- let(:another_user) { User.create!(name: 'Another Guy', email: 'anotherguy@randomemail.com') }
87
- let(:post) { Post.create!(title: 'Test', body: 'Post', modifier: user, views: 100) }
88
- let(:comment) { post.comments.create!(title: 'test', body: 'comment', modifier: user) }
89
- let(:tag) { Tag.create!(title: 'test', updated_by: user) }
90
-
91
- describe 'track' do
92
- describe 'on creation' do
93
- it 'should have one history track in comment' do
94
- expect(comment.history_tracks.count).to eq(1)
95
- end
96
-
97
- it 'should assign title and body on modified' do
98
- expect(comment.history_tracks.first.modified).to eq('t' => 'test', 'body' => 'comment')
99
- end
100
-
101
- it 'should not assign title and body on original' do
102
- expect(comment.history_tracks.first.original).to eq({})
103
- end
104
-
105
- it 'should assign modifier' do
106
- expect(comment.history_tracks.first.modifier.id).to eq(user.id)
107
- end
108
-
109
- it 'should assign version' do
110
- expect(comment.history_tracks.first.version).to eq(1)
111
- end
112
-
113
- it 'should assign scope' do
114
- expect(comment.history_tracks.first.scope).to eq('post')
115
- end
116
-
117
- it 'should assign method' do
118
- expect(comment.history_tracks.first.action).to eq('create')
119
- end
120
-
121
- it 'should assign association_chain' do
122
- expected = [
123
- { 'id' => post.id, 'name' => 'Post' },
124
- { 'id' => comment.id, 'name' => 'coms' }
125
- ]
126
- expect(comment.history_tracks.first.association_chain).to eq(expected)
127
- end
128
- end
129
-
130
- describe 'on destruction' do
131
- it 'should have two history track records in post' do
132
- post # This will create history track records for creation
133
- expect do
134
- post.destroy
135
- end.to change(Tracker, :count).by(1)
136
- end
137
-
138
- it 'should assign destroy on track record' do
139
- post.destroy
140
- expect(post.history_tracks.last.action).to eq('destroy')
141
- end
142
-
143
- it 'should return affected attributes from track record' do
144
- post.destroy
145
- expect(post.history_tracks.last.affected['title']).to eq('Test')
146
- end
147
-
148
- it 'should no-op on repeated calls to destroy' do
149
- post.destroy
150
- expect do
151
- post.destroy
152
- end.not_to change(Tracker, :count)
153
- end
154
- end
155
-
156
- describe 'on update non-embedded' do
157
- it 'should create a history track if changed attributes match tracked attributes' do
158
- post # This will create history track records for creation
159
- expect do
160
- post.update_attributes!(title: 'Another Test')
161
- end.to change(Tracker, :count).by(1)
162
- end
163
-
164
- it 'should not create a history track if changed attributes do not match tracked attributes' do
165
- post # This will create history track records for creation
166
- expect do
167
- post.update_attributes!(rating: 'untracked')
168
- end.to change(Tracker, :count).by(0)
169
- end
170
-
171
- it 'should assign modified fields' do
172
- post.update_attributes!(title: 'Another Test')
173
- expect(post.history_tracks.last.modified).to eq(
174
- 'title' => 'Another Test'
175
- )
176
- end
177
-
178
- it 'should assign method field' do
179
- post.update_attributes!(title: 'Another Test')
180
- expect(post.history_tracks.last.action).to eq('update')
181
- end
182
-
183
- it 'should assign original fields' do
184
- post.update_attributes!(title: 'Another Test')
185
- expect(post.history_tracks.last.original).to eq(
186
- 'title' => 'Test'
187
- )
188
- end
189
-
190
- it 'should assign modifier' do
191
- post.update_attributes!(title: 'Another Test')
192
- expect(post.history_tracks.first.modifier.id).to eq(user.id)
193
- end
194
-
195
- it 'should assign version on history tracks' do
196
- post.update_attributes!(title: 'Another Test')
197
- expect(post.history_tracks.first.version).to eq(1)
198
- end
199
-
200
- it 'should assign version on post' do
201
- expect(post.version).to eq(1) # Created
202
- post.update_attributes!(title: 'Another Test')
203
- expect(post.version).to eq(2) # Updated
204
- end
205
-
206
- it 'should assign scope' do
207
- post.update_attributes!(title: 'Another Test')
208
- expect(post.history_tracks.first.scope).to eq('post')
209
- end
210
-
211
- it 'should assign association_chain' do
212
- post.update_attributes!(title: 'Another Test')
213
- expect(post.history_tracks.last.association_chain).to eq([{ 'id' => post.id, 'name' => 'Post' }])
214
- end
215
-
216
- it 'should exclude defined options' do
217
- name = user.name
218
- user.update_attributes!(name: 'Aaron2', email: 'aaronsnewemail@randomemail.com')
219
- expect(user.history_tracks.last.original.keys).to eq(['n'])
220
- expect(user.history_tracks.last.original['n']).to eq(name)
221
- expect(user.history_tracks.last.modified.keys).to eq(['n'])
222
- expect(user.history_tracks.last.modified['n']).to eq(user.name)
223
- end
224
-
225
- it 'should undo field changes' do
226
- name = user.name
227
- user.update_attributes!(name: 'Aaron2', email: 'aaronsnewemail@randomemail.com')
228
- user.history_tracks.last.undo! nil
229
- expect(user.reload.name).to eq(name)
230
- end
231
-
232
- it 'should undo non-existing field changes' do
233
- post = Post.create!(modifier: user, views: 100)
234
- expect(post.reload.title).to be_nil
235
- post.update_attributes!(title: 'Aaron2')
236
- expect(post.reload.title).to eq('Aaron2')
237
- post.history_tracks.last.undo! user
238
- expect(post.reload.title).to be_nil
239
- end
240
-
241
- it 'should track array changes' do
242
- aliases = user.aliases
243
- user.update_attributes!(aliases: %w[bob joe])
244
- expect(user.history_tracks.last.original['aliases']).to eq(aliases)
245
- expect(user.history_tracks.last.modified['aliases']).to eq(user.aliases)
246
- end
247
-
248
- it 'should undo array changes' do
249
- aliases = user.aliases
250
- user.update_attributes!(aliases: %w[bob joe])
251
- user.history_tracks.last.undo! nil
252
- expect(user.reload.aliases).to eq(aliases)
253
- end
254
- end
255
-
256
- describe '#tracked_changes' do
257
- context 'create action' do
258
- subject { tag.history_tracks.first.tracked_changes }
259
- it 'consider all fields values as :to' do
260
- expect(subject[:title]).to eq({ to: 'test' }.with_indifferent_access)
261
- end
262
- end
263
- context 'destroy action' do
264
- subject do
265
- tag.destroy
266
- tag.history_tracks.last.tracked_changes
267
- end
268
- it 'consider all fields values as :from' do
269
- expect(subject[:title]).to eq({ from: 'test' }.with_indifferent_access)
270
- end
271
- end
272
- context 'update action' do
273
- subject { user.history_tracks.last.tracked_changes }
274
- before do
275
- user.update_attributes!(name: 'Aaron2', email: nil, country: '', city: nil, phone: '867-5309', aliases: ['', 'bill', 'james'])
276
- end
277
- it { is_expected.to be_a HashWithIndifferentAccess }
278
- it 'should track changed field' do
279
- expect(subject[:n]).to eq({ from: 'Aaron', to: 'Aaron2' }.with_indifferent_access)
280
- end
281
- it 'should track added field' do
282
- expect(subject[:phone]).to eq({ to: '867-5309' }.with_indifferent_access)
283
- end
284
- it 'should track removed field' do
285
- expect(subject[:city]).to eq({ from: 'Toronto' }.with_indifferent_access)
286
- end
287
- it 'should not consider blank as removed' do
288
- expect(subject[:country]).to eq({ from: 'Canada', to: '' }.with_indifferent_access)
289
- end
290
- it 'should track changed array field' do
291
- expect(subject[:aliases]).to eq({ from: ['bob'], to: ['', 'bill', 'james'] }.with_indifferent_access)
292
- end
293
- it 'should not track unmodified field' do
294
- expect(subject[:address]).to be_nil
295
- end
296
- it 'should not track untracked fields' do
297
- expect(subject[:email]).to be_nil
298
- end
299
- end
300
- end
301
-
302
- describe '#tracked_edits' do
303
- context 'create action' do
304
- subject { tag.history_tracks.first.tracked_edits }
305
- it 'consider all edits as ;add' do
306
- expect(subject[:add]).to eq({ title: 'test' }.with_indifferent_access)
307
- end
308
- end
309
- context 'destroy action' do
310
- subject do
311
- tag.destroy
312
- tag.history_tracks.last.tracked_edits
313
- end
314
- it 'consider all edits as ;remove' do
315
- expect(subject[:remove]).to eq({ title: 'test' }.with_indifferent_access)
316
- end
317
- end
318
- context 'update action' do
319
- subject { user.history_tracks.last.tracked_edits }
320
- before do
321
- user.update_attributes!(name: 'Aaron2', email: nil, country: '', city: nil, phone: '867-5309', aliases: ['', 'bill', 'james'])
322
- end
323
- it { is_expected.to be_a HashWithIndifferentAccess }
324
- it 'should track changed field' do
325
- expect(subject[:modify]).to eq({ n: { from: 'Aaron', to: 'Aaron2' } }.with_indifferent_access)
326
- end
327
- it 'should track added field' do
328
- expect(subject[:add]).to eq({ phone: '867-5309' }.with_indifferent_access)
329
- end
330
- it 'should track removed field and consider blank as removed' do
331
- expect(subject[:remove]).to eq({ city: 'Toronto', country: 'Canada' }.with_indifferent_access)
332
- end
333
- it 'should track changed array field' do
334
- expect(subject[:array]).to eq({ aliases: { remove: ['bob'], add: ['', 'bill', 'james'] } }.with_indifferent_access)
335
- end
336
- it 'should not track unmodified field' do
337
- %w[add modify remove array].each do |edit|
338
- expect(subject[edit][:address]).to be_nil
339
- end
340
- end
341
- it 'should not track untracked fields' do
342
- %w[add modify remove array].each do |edit|
343
- expect(subject[edit][:email]).to be_nil
344
- end
345
- end
346
- end
347
- context 'with empty values' do
348
- before do
349
- allow(subject).to receive(:trackable_parent_class) { Tracker }
350
- allow(Tracker).to receive(:tracked_embeds_many?) { false }
351
- end
352
- subject { Tracker.new }
353
- it 'should skip empty values' do
354
- allow(subject).to receive(:tracked_changes) { { name: { to: '', from: [] }, city: { to: 'Toronto', from: '' } } }
355
- expect(subject.tracked_edits).to eq({ add: { city: 'Toronto' } }.with_indifferent_access)
356
- end
357
- end
358
- end
359
-
360
- describe 'on update non-embedded twice' do
361
- it 'should assign version on post' do
362
- expect(post.version).to eq(1)
363
- post.update_attributes!(title: 'Test2')
364
- post.update_attributes!(title: 'Test3')
365
- expect(post.version).to eq(3)
366
- end
367
-
368
- it 'should create a history track if changed attributes match tracked attributes' do
369
- post # Created
370
- expect do
371
- post.update_attributes!(title: 'Test2')
372
- post.update_attributes!(title: 'Test3')
373
- end.to change(Tracker, :count).by(2)
374
- end
375
-
376
- it 'should create a history track of version 2' do
377
- post.update_attributes!(title: 'Test2')
378
- post.update_attributes!(title: 'Test3')
379
- expect(post.history_tracks.where(version: 2).first).not_to be_nil
380
- end
381
-
382
- it 'should assign modified fields' do
383
- post.update_attributes!(title: 'Test2')
384
- post.update_attributes!(title: 'Test3')
385
- expect(post.history_tracks.where(version: 3).first.modified).to eq(
386
- 'title' => 'Test3'
387
- )
388
- end
389
-
390
- it 'should assign original fields' do
391
- post.update_attributes!(title: 'Test2')
392
- post.update_attributes!(title: 'Test3')
393
- expect(post.history_tracks.where(version: 3).first.original).to eq(
394
- 'title' => 'Test2'
395
- )
396
- end
397
-
398
- it 'should assign modifier' do
399
- post.update_attributes!(title: 'Another Test', modifier: another_user)
400
- expect(post.history_tracks.last.modifier.id).to eq(another_user.id)
401
- end
402
- end
403
-
404
- describe 'on update embedded 1..N (embeds_many)' do
405
- it 'should assign version on comment' do
406
- comment.update_attributes!(title: 'Test2')
407
- expect(comment.version).to eq(2) # first track generated on creation
408
- end
409
-
410
- it 'should create a history track of version 2' do
411
- comment.update_attributes!(title: 'Test2')
412
- expect(comment.history_tracks.where(version: 2).first).not_to be_nil
413
- end
414
-
415
- it 'should assign modified fields' do
416
- comment.update_attributes!(t: 'Test2')
417
- expect(comment.history_tracks.where(version: 2).first.modified).to eq(
418
- 't' => 'Test2'
419
- )
420
- end
421
-
422
- it 'should assign original fields' do
423
- comment.update_attributes!(title: 'Test2')
424
- expect(comment.history_tracks.where(version: 2).first.original).to eq(
425
- 't' => 'test'
426
- )
427
- end
428
-
429
- it 'should be possible to undo from parent' do
430
- comment.update_attributes!(title: 'Test 2')
431
- user
432
- post.history_tracks.last.undo!(user)
433
- comment.reload
434
- expect(comment.title).to eq('test')
435
- end
436
-
437
- it 'should assign modifier' do
438
- post.update_attributes!(title: 'Another Test', modifier: another_user)
439
- expect(post.history_tracks.last.modifier.id).to eq(another_user.id)
440
- end
441
- end
442
-
443
- describe 'on update embedded 1..1 (embeds_one)' do
444
- let(:section) { Section.new(title: 'Technology', modifier: user) }
445
-
446
- before(:each) do
447
- post.section = section
448
- post.modifier = user
449
- post.save!
450
- post.reload
451
- post.section
452
- end
453
-
454
- it 'should assign version on create section' do
455
- expect(section.version).to eq(1)
456
- end
457
-
458
- it 'should assign version on section' do
459
- section.update_attributes!(title: 'Technology 2')
460
- expect(section.version).to eq(2) # first track generated on creation
461
- end
462
-
463
- it 'should create a history track of version 2' do
464
- section.update_attributes!(title: 'Technology 2')
465
- expect(section.history_tracks.where(version: 2).first).not_to be_nil
466
- end
467
-
468
- it 'should assign modified fields' do
469
- section.update_attributes!(title: 'Technology 2')
470
- expect(section.history_tracks.where(version: 2).first.modified).to eq(
471
- 't' => 'Technology 2'
472
- )
473
- end
474
-
475
- it 'should assign original fields' do
476
- section.update_attributes!(title: 'Technology 2')
477
- expect(section.history_tracks.where(version: 2).first.original).to eq(
478
- 't' => 'Technology'
479
- )
480
- end
481
-
482
- it 'should be possible to undo from parent' do
483
- section.update_attributes!(title: 'Technology 2')
484
- post.history_tracks.last.undo!(user)
485
- section.reload
486
- expect(section.title).to eq('Technology')
487
- end
488
-
489
- it 'should assign modifier' do
490
- section.update_attributes!(title: 'Business', modifier: another_user)
491
- expect(post.history_tracks.last.modifier.id).to eq(another_user.id)
492
- end
493
- end
494
-
495
- describe 'on destroy embedded' do
496
- it 'should be possible to re-create destroyed embedded' do
497
- comment.destroy
498
- comment.history_tracks.last.undo!(user)
499
- post.reload
500
- expect(post.comments.first.title).to eq('test')
501
- end
502
-
503
- it 'should be possible to re-create destroyed embedded from parent' do
504
- comment.destroy
505
- post.history_tracks.last.undo!(user)
506
- post.reload
507
- expect(post.comments.first.title).to eq('test')
508
- end
509
-
510
- it 'should be possible to destroy after re-create embedded from parent' do
511
- comment.destroy
512
- post.history_tracks.last.undo!(user)
513
- post.history_tracks.last.undo!(user)
514
- post.reload
515
- expect(post.comments.count).to eq(0)
516
- end
517
-
518
- it 'should be possible to create with redo after undo create embedded from parent' do
519
- comment # initialize
520
- post.comments.create!(title: 'The second one', modifier: user)
521
- track = post.history_tracks[2]
522
- expect(post.reload.comments.count).to eq 2
523
- track.undo!(user)
524
- expect(post.reload.comments.count).to eq 1
525
- track.redo!(user)
526
- expect(post.reload.comments.count).to eq 2
527
- end
528
- end
529
-
530
- describe 'embedded with cascading callbacks' do
531
- let(:tag_foo) { post.tags.create!(title: 'foo', updated_by: user) }
532
- let(:tag_bar) { post.tags.create!(title: 'bar', updated_by: user) }
533
-
534
- it 'should allow an update through the parent model' do
535
- update_hash = { 'post' => { 'tags_attributes' => { '1234' => { 'id' => tag_bar.id, 'title' => 'baz' } } } }
536
- post.update_attributes!(update_hash['post'])
537
- expect(post.tags.last.title).to eq('baz')
538
- end
539
-
540
- it 'should be possible to destroy through parent model using canoncial _destroy macro' do
541
- tag_foo
542
- tag_bar # initialize
543
- expect(post.tags.count).to eq(2)
544
- update_hash = { 'post' => { 'tags_attributes' => { '1234' => { 'id' => tag_bar.id, 'title' => 'baz', '_destroy' => 'true' } } } }
545
- post.update_attributes!(update_hash['post'])
546
- expect(post.tags.count).to eq(1)
547
- expect(post.history_tracks.to_a.last.action).to eq('destroy')
548
- end
549
-
550
- it 'should write relationship name for association_chain hiearchy instead of class name when using _destroy macro' do
551
- update_hash = { 'tags_attributes' => { '1234' => { 'id' => tag_foo.id, '_destroy' => '1' } } }
552
- post.update_attributes!(update_hash)
553
-
554
- # historically this would have evaluated to 'Tags' and an error would be thrown
555
- # on any call that walked up the association_chain, e.g. 'trackable'
556
- expect(tag_foo.history_tracks.last.association_chain.last['name']).to eq('tags')
557
- expect { tag_foo.history_tracks.last.trackable }.not_to raise_error
558
- end
559
- end
560
-
561
- describe 'non-embedded' do
562
- it 'should undo changes' do
563
- post.update_attributes!(title: 'Test2')
564
- post.history_tracks.where(version: 2).last.undo!(user)
565
- post.reload
566
- expect(post.title).to eq('Test')
567
- end
568
-
569
- it 'should undo destruction' do
570
- post.destroy
571
- post.history_tracks.where(version: 2).last.undo!(user)
572
- expect(Post.find(post.id).title).to eq('Test')
573
- end
574
-
575
- it 'should create a new history track after undo' do
576
- comment # initialize
577
- post.update_attributes!(title: 'Test2')
578
- post.history_tracks.last.undo!(user)
579
- post.reload
580
- expect(post.history_tracks.count).to eq(4)
581
- end
582
-
583
- it 'should assign user as the modifier of the newly created history track' do
584
- post.update_attributes!(title: 'Test2')
585
- post.history_tracks.where(version: 2).last.undo!(user)
586
- post.reload
587
- expect(post.history_tracks.where(version: 2).last.modifier.id).to eq(user.id)
588
- end
589
-
590
- it 'should stay the same after undo and redo' do
591
- post.update_attributes!(title: 'Test2')
592
- track = post.history_tracks.last
593
- track.undo!(user)
594
- track.redo!(user)
595
- post2 = Post.where(_id: post.id).first
596
-
597
- expect(post.title).to eq(post2.title)
598
- end
599
-
600
- it 'should be destroyed after undo and redo' do
601
- post.destroy
602
- track = post.history_tracks.where(version: 2).last
603
- track.undo!(user)
604
- track.redo!(user)
605
- expect(Post.where(_id: post.id).first).to be_nil
606
- end
607
- end
608
-
609
- describe 'embedded' do
610
- it 'should undo changes' do
611
- comment.update_attributes!(title: 'Test2')
612
- comment.history_tracks.where(version: 2).first.undo!(user)
613
- comment.reload
614
- expect(comment.title).to eq('test')
615
- end
616
-
617
- it 'should create a new history track after undo' do
618
- comment.update_attributes!(title: 'Test2')
619
- comment.history_tracks.where(version: 2).first.undo!(user)
620
- comment.reload
621
- expect(comment.history_tracks.count).to eq(3)
622
- end
623
-
624
- it 'should assign user as the modifier of the newly created history track' do
625
- comment.update_attributes!(title: 'Test2')
626
- comment.history_tracks.where(version: 2).first.undo!(user)
627
- comment.reload
628
- expect(comment.history_tracks.where(version: 3).first.modifier.id).to eq(user.id)
629
- end
630
-
631
- it 'should stay the same after undo and redo' do
632
- comment.update_attributes!(title: 'Test2')
633
- track = comment.history_tracks.where(version: 2).first
634
- track.undo!(user)
635
- track.redo!(user)
636
- comment.reload
637
- expect(comment.title).to eq('Test2')
638
- end
639
- end
640
-
641
- describe 'trackables' do
642
- before :each do
643
- comment.update_attributes!(title: 'Test2') # version == 2
644
- comment.update_attributes!(title: 'Test3') # version == 3
645
- comment.update_attributes!(title: 'Test4') # version == 4
646
- end
647
-
648
- describe 'undo' do
649
- { 'undo' => [nil], 'undo!' => [nil, :reload] }.each do |test_method, methods|
650
- methods.each do |method|
651
- context (method || 'instance').to_s do
652
- it 'recognizes :from, :to options' do
653
- comment.send test_method, user, from: 4, to: 2
654
- comment.send(method) if method
655
- expect(comment.title).to eq('test')
656
- end
657
-
658
- it 'recognizes parameter as version number' do
659
- comment.send test_method, user, 3
660
- comment.send(method) if method
661
- expect(comment.title).to eq('Test2')
662
- end
663
-
664
- it 'should undo last version when no parameter is specified' do
665
- comment.send test_method, user
666
- comment.send(method) if method
667
- expect(comment.title).to eq('Test3')
668
- end
669
-
670
- it 'recognizes :last options' do
671
- comment.send test_method, user, last: 2
672
- comment.send(method) if method
673
- expect(comment.title).to eq('Test2')
674
- end
675
-
676
- if Mongoid::Compatibility::Version.mongoid3?
677
- context 'protected attributes' do
678
- before :each do
679
- Comment.attr_accessible(nil)
680
- end
681
-
682
- after :each do
683
- Comment.attr_protected(nil)
684
- end
685
-
686
- it 'should undo last version when no parameter is specified on protected attributes' do
687
- comment.send test_method, user
688
- comment.send(method) if method
689
- expect(comment.title).to eq('Test3')
690
- end
691
-
692
- it 'recognizes :last options on model with protected attributes' do
693
- comment.send test_method, user, last: 2
694
- comment.send(method) if method
695
- expect(comment.title).to eq('Test2')
696
- end
697
- end
698
- end
699
- end
700
- end
701
- end
702
- end
703
-
704
- describe 'redo' do
705
- [nil, :reload].each do |method|
706
- context (method || 'instance').to_s do
707
- before :each do
708
- comment.update_attributes!(title: 'Test5')
709
- end
710
-
711
- it 'should recognize :from, :to options' do
712
- comment.redo! user, from: 2, to: 4
713
- comment.send(method) if method
714
- expect(comment.title).to eq('Test4')
715
- end
716
-
717
- it 'should recognize parameter as version number' do
718
- comment.redo! user, 2
719
- comment.send(method) if method
720
- expect(comment.title).to eq('Test2')
721
- end
722
-
723
- it 'should redo last version when no parameter is specified' do
724
- comment.redo! user
725
- comment.send(method) if method
726
- expect(comment.title).to eq('Test5')
727
- end
728
-
729
- it 'should recognize :last options' do
730
- comment.redo! user, last: 1
731
- comment.send(method) if method
732
- expect(comment.title).to eq('Test5')
733
- end
734
-
735
- if Mongoid::Compatibility::Version.mongoid3?
736
- context 'protected attributes' do
737
- before :each do
738
- Comment.attr_accessible(nil)
739
- end
740
-
741
- after :each do
742
- Comment.attr_protected(nil)
743
- end
744
-
745
- it 'should recognize parameter as version number' do
746
- comment.redo! user, 2
747
- comment.send(method) if method
748
- expect(comment.title).to eq('Test2')
749
- end
750
-
751
- it 'should recognize :from, :to options' do
752
- comment.redo! user, from: 2, to: 4
753
- comment.send(method) if method
754
- expect(comment.title).to eq('Test4')
755
- end
756
- end
757
- end
758
- end
759
- end
760
- end
761
- end
762
-
763
- describe 'embedded with a polymorphic trackable' do
764
- let(:foo) { Foo.new(title: 'a title', body: 'a body', modifier: user) }
765
- before :each do
766
- post.comments << foo
767
- post.save!
768
- end
769
- it 'should assign interface name in association chain' do
770
- foo.update_attribute(:body, 'a changed body')
771
- expected_root = { 'name' => 'Post', 'id' => post.id }
772
- expected_node = { 'name' => 'coms', 'id' => foo.id }
773
- expect(foo.history_tracks.first.association_chain).to eq([expected_root, expected_node])
774
- end
775
- end
776
-
777
- describe '#trackable_parent_class' do
778
- context 'a non-embedded model' do
779
- it 'should return the trackable parent class' do
780
- expect(tag.history_tracks.first.trackable_parent_class).to eq(Tag)
781
- end
782
- it 'should return the parent class even if the trackable is deleted' do
783
- tracker = tag.history_tracks.first
784
- tag.destroy
785
- expect(tracker.trackable_parent_class).to eq(Tag)
786
- end
787
- end
788
- context 'an embedded model' do
789
- it 'should return the trackable parent class' do
790
- comment.update_attributes!(title: 'Foo')
791
- expect(comment.history_tracks.first.trackable_parent_class).to eq(Post)
792
- end
793
- it 'should return the parent class even if the trackable is deleted' do
794
- tracker = comment.history_tracks.first
795
- comment.destroy
796
- expect(tracker.trackable_parent_class).to eq(Post)
797
- end
798
- end
799
- end
800
-
801
- describe 'when default scope is present' do
802
- before :each do
803
- class Post
804
- default_scope -> { where(title: nil) }
805
- end
806
- class Comment
807
- default_scope -> { where(title: nil) }
808
- end
809
- class User
810
- default_scope -> { where(name: nil) }
811
- end
812
- class Tag
813
- default_scope -> { where(title: nil) }
814
- end
815
- end
816
-
817
- describe 'post' do
818
- it 'should correctly undo and redo' do
819
- post.update_attributes!(title: 'a new title')
820
- track = post.history_tracks.last
821
- track.undo! user
822
- expect(post.reload.title).to eq('Test')
823
- track.redo! user
824
- expect(post.reload.title).to eq('a new title')
825
- end
826
-
827
- it 'should stay the same after undo and redo' do
828
- post.update_attributes!(title: 'testing')
829
- track = post.history_tracks.last
830
- track.undo! user
831
- track.redo! user
832
- expect(post.reload.title).to eq('testing')
833
- end
834
- end
835
- describe 'comment' do
836
- it 'should correctly undo and redo' do
837
- comment.update_attributes!(title: 'a new title')
838
- track = comment.history_tracks.last
839
- track.undo! user
840
- expect(comment.reload.title).to eq('test')
841
- track.redo! user
842
- expect(comment.reload.title).to eq('a new title')
843
- end
844
-
845
- it 'should stay the same after undo and redo' do
846
- comment.update_attributes!(title: 'testing')
847
- track = comment.history_tracks.last
848
- track.undo! user
849
- track.redo! user
850
- expect(comment.reload.title).to eq('testing')
851
- end
852
- end
853
- describe 'user' do
854
- it 'should correctly undo and redo' do
855
- user.update_attributes!(name: 'a new name')
856
- track = user.history_tracks.last
857
- track.undo! user
858
- expect(user.reload.name).to eq('Aaron')
859
- track.redo! user
860
- expect(user.reload.name).to eq('a new name')
861
- end
862
-
863
- it 'should stay the same after undo and redo' do
864
- user.update_attributes!(name: 'testing')
865
- track = user.history_tracks.last
866
- track.undo! user
867
- track.redo! user
868
- expect(user.reload.name).to eq('testing')
869
- end
870
- end
871
- describe 'tag' do
872
- it 'should correctly undo and redo' do
873
- tag.update_attributes!(title: 'a new title')
874
- track = tag.history_tracks.last
875
- track.undo! user
876
- expect(tag.reload.title).to eq('test')
877
- track.redo! user
878
- expect(tag.reload.title).to eq('a new title')
879
- end
880
-
881
- it 'should stay the same after undo and redo' do
882
- tag.update_attributes!(title: 'testing')
883
- track = tag.history_tracks.last
884
- track.undo! user
885
- track.redo! user
886
- expect(tag.reload.title).to eq('testing')
887
- end
888
- end
889
- end
890
-
891
- describe 'overriden changes_method with additional fields' do
892
- before :each do
893
- class OverriddenChangesMethod
894
- include Mongoid::Document
895
- include Mongoid::History::Trackable
896
-
897
- track_history on: [:foo], changes_method: :my_changes
898
-
899
- def my_changes
900
- { foo: %w[bar baz] }
901
- end
902
- end
903
- end
904
-
905
- after :each do
906
- Object.send(:remove_const, :OverriddenChangesMethod)
907
- end
908
-
909
- it 'should add foo to the changes history' do
910
- o = OverriddenChangesMethod.create(modifier: user)
911
- o.save!
912
- track = o.history_tracks.last
913
- expect(track.modified).to eq('foo' => 'baz')
914
- expect(track.original).to eq('foo' => 'bar')
915
- end
916
- end
917
-
918
- describe 'localized fields' do
919
- before :each do
920
- class Sausage
921
- include Mongoid::Document
922
- include Mongoid::History::Trackable
923
-
924
- field :flavour, localize: true
925
- track_history on: [:flavour], track_destroy: true, modifier_field_optional: true
926
- end
927
- end
928
-
929
- after :each do
930
- Object.send(:remove_const, :Sausage)
931
- end
932
-
933
- it 'should correctly undo and redo' do
934
- pending unless Sausage.respond_to?(:localized_fields)
935
-
936
- sausage = Sausage.create!(flavour_translations: { 'en' => 'Apple', 'nl' => 'Appel' }, modifier: user)
937
- sausage.update_attributes!(flavour: 'Guinness')
938
-
939
- track = sausage.history_tracks.last
940
-
941
- track.undo! user
942
- expect(sausage.reload.flavour).to eq('Apple')
943
-
944
- track.redo! user
945
- expect(sausage.reload.flavour).to eq('Guinness')
946
-
947
- sausage.destroy
948
- expect(sausage.history_tracks.last.action).to eq('destroy')
949
- sausage.history_tracks.last.undo! user
950
- expect(sausage.reload.flavour).to eq('Guinness')
951
- end
952
- end
953
-
954
- describe 'changing collection' do
955
- before :each do
956
- class Fish
957
- include Mongoid::Document
958
- include Mongoid::History::Trackable
959
-
960
- track_history on: [:species], modifier_field_optional: true
961
- store_in collection: :animals
962
-
963
- field :species
964
- end
965
- end
966
-
967
- after :each do
968
- Object.send(:remove_const, :Fish)
969
- end
970
-
971
- it 'should track history' do
972
- Fish.new.save!
973
- end
974
- end
975
- end
976
- end
1
+ require 'spec_helper'
2
+
3
+ describe Mongoid::History do
4
+ before :each do
5
+ class Post
6
+ include Mongoid::Document
7
+ include Mongoid::Timestamps
8
+ include Mongoid::History::Trackable
9
+
10
+ field :title
11
+ field :body
12
+ field :rating
13
+ field :views, type: Integer
14
+
15
+ embeds_many :comments, store_as: :coms
16
+ embeds_one :section, store_as: :sec
17
+ embeds_many :tags, cascade_callbacks: true
18
+
19
+ accepts_nested_attributes_for :tags, allow_destroy: true
20
+
21
+ track_history on: %i[title body], track_destroy: true
22
+ end
23
+
24
+ class Comment
25
+ include Mongoid::Document
26
+ include Mongoid::Timestamps
27
+ include Mongoid::History::Trackable
28
+
29
+ field :t, as: :title
30
+ field :body
31
+ embedded_in :commentable, polymorphic: true
32
+ # BUG: see https://github.com/mongoid/mongoid-history/issues/223, modifier_field_optional should not be necessary
33
+ track_history on: %i[title body], scope: :post, track_create: true, track_destroy: true, modifier_field_optional: true
34
+ end
35
+
36
+ class Section
37
+ include Mongoid::Document
38
+ include Mongoid::Timestamps
39
+ include Mongoid::History::Trackable
40
+
41
+ field :t, as: :title
42
+ embedded_in :post
43
+ track_history on: [:title], scope: :post, track_create: true, track_destroy: true
44
+ end
45
+
46
+ class User
47
+ include Mongoid::Document
48
+ include Mongoid::Timestamps
49
+ include Mongoid::History::Trackable
50
+
51
+ field :n, as: :name
52
+ field :em, as: :email
53
+ field :phone
54
+ field :address
55
+ field :city
56
+ field :country
57
+ field :aliases, type: Array
58
+ track_history except: %i[email updated_at], modifier_field_optional: true
59
+ end
60
+
61
+ class Tag
62
+ include Mongoid::Document
63
+ # include Mongoid::Timestamps (see: https://github.com/mongoid/mongoid/issues/3078)
64
+ include Mongoid::History::Trackable
65
+
66
+ belongs_to :updated_by, class_name: 'User'
67
+
68
+ field :title
69
+ track_history on: [:title], scope: :post, track_create: true, track_destroy: true, modifier_field: :updated_by
70
+ end
71
+
72
+ class Foo < Comment
73
+ end
74
+ end
75
+
76
+ after :each do
77
+ Object.send(:remove_const, :Post)
78
+ Object.send(:remove_const, :Comment)
79
+ Object.send(:remove_const, :Section)
80
+ Object.send(:remove_const, :User)
81
+ Object.send(:remove_const, :Tag)
82
+ Object.send(:remove_const, :Foo)
83
+ end
84
+
85
+ let(:user) { User.create!(name: 'Aaron', email: 'aaron@randomemail.com', aliases: ['bob'], country: 'Canada', city: 'Toronto', address: '21 Jump Street') }
86
+ let(:another_user) { User.create!(name: 'Another Guy', email: 'anotherguy@randomemail.com') }
87
+ let(:post) { Post.create!(title: 'Test', body: 'Post', modifier: user, views: 100) }
88
+ let(:comment) { post.comments.create!(title: 'test', body: 'comment', modifier: user) }
89
+ let(:tag) { Tag.create!(title: 'test', updated_by: user) }
90
+
91
+ describe 'track' do
92
+ describe 'on creation' do
93
+ it 'should have one history track in comment' do
94
+ expect(comment.history_tracks.count).to eq(1)
95
+ end
96
+
97
+ it 'should assign title and body on modified' do
98
+ expect(comment.history_tracks.first.modified).to eq('t' => 'test', 'body' => 'comment')
99
+ end
100
+
101
+ it 'should not assign title and body on original' do
102
+ expect(comment.history_tracks.first.original).to eq({})
103
+ end
104
+
105
+ it 'should assign modifier' do
106
+ expect(comment.history_tracks.first.modifier.id).to eq(user.id)
107
+ end
108
+
109
+ it 'should assign version' do
110
+ expect(comment.history_tracks.first.version).to eq(1)
111
+ end
112
+
113
+ it 'should assign scope' do
114
+ expect(comment.history_tracks.first.scope).to eq('post')
115
+ end
116
+
117
+ it 'should assign method' do
118
+ expect(comment.history_tracks.first.action).to eq('create')
119
+ end
120
+
121
+ it 'should assign association_chain' do
122
+ expected = [
123
+ { 'id' => post.id, 'name' => 'Post' },
124
+ { 'id' => comment.id, 'name' => 'coms' }
125
+ ]
126
+ expect(comment.history_tracks.first.association_chain).to eq(expected)
127
+ end
128
+ end
129
+
130
+ describe 'on destruction' do
131
+ it 'should have two history track records in post' do
132
+ post # This will create history track records for creation
133
+ expect do
134
+ post.destroy
135
+ end.to change(Tracker, :count).by(1)
136
+ end
137
+
138
+ it 'should assign destroy on track record' do
139
+ post.destroy
140
+ expect(post.history_tracks.last.action).to eq('destroy')
141
+ end
142
+
143
+ it 'should return affected attributes from track record' do
144
+ post.destroy
145
+ expect(post.history_tracks.last.affected['title']).to eq('Test')
146
+ end
147
+
148
+ it 'should no-op on repeated calls to destroy' do
149
+ post.destroy
150
+ expect do
151
+ post.destroy
152
+ end.not_to change(Tracker, :count)
153
+ end
154
+ end
155
+
156
+ describe 'on update non-embedded' do
157
+ it 'should create a history track if changed attributes match tracked attributes' do
158
+ post # This will create history track records for creation
159
+ expect do
160
+ post.update_attributes!(title: 'Another Test')
161
+ end.to change(Tracker, :count).by(1)
162
+ end
163
+
164
+ it 'should not create a history track if changed attributes do not match tracked attributes' do
165
+ post # This will create history track records for creation
166
+ expect do
167
+ post.update_attributes!(rating: 'untracked')
168
+ end.to change(Tracker, :count).by(0)
169
+ end
170
+
171
+ it 'should assign modified fields' do
172
+ post.update_attributes!(title: 'Another Test')
173
+ expect(post.history_tracks.last.modified).to eq(
174
+ 'title' => 'Another Test'
175
+ )
176
+ end
177
+
178
+ it 'should assign method field' do
179
+ post.update_attributes!(title: 'Another Test')
180
+ expect(post.history_tracks.last.action).to eq('update')
181
+ end
182
+
183
+ it 'should assign original fields' do
184
+ post.update_attributes!(title: 'Another Test')
185
+ expect(post.history_tracks.last.original).to eq(
186
+ 'title' => 'Test'
187
+ )
188
+ end
189
+
190
+ it 'should assign modifier' do
191
+ post.update_attributes!(title: 'Another Test')
192
+ expect(post.history_tracks.first.modifier.id).to eq(user.id)
193
+ end
194
+
195
+ it 'should assign version on history tracks' do
196
+ post.update_attributes!(title: 'Another Test')
197
+ expect(post.history_tracks.first.version).to eq(1)
198
+ end
199
+
200
+ it 'should assign version on post' do
201
+ expect(post.version).to eq(1) # Created
202
+ post.update_attributes!(title: 'Another Test')
203
+ expect(post.version).to eq(2) # Updated
204
+ end
205
+
206
+ it 'should assign scope' do
207
+ post.update_attributes!(title: 'Another Test')
208
+ expect(post.history_tracks.first.scope).to eq('post')
209
+ end
210
+
211
+ it 'should assign association_chain' do
212
+ post.update_attributes!(title: 'Another Test')
213
+ expect(post.history_tracks.last.association_chain).to eq([{ 'id' => post.id, 'name' => 'Post' }])
214
+ end
215
+
216
+ it 'should exclude defined options' do
217
+ name = user.name
218
+ user.update_attributes!(name: 'Aaron2', email: 'aaronsnewemail@randomemail.com')
219
+ expect(user.history_tracks.last.original.keys).to eq(['n'])
220
+ expect(user.history_tracks.last.original['n']).to eq(name)
221
+ expect(user.history_tracks.last.modified.keys).to eq(['n'])
222
+ expect(user.history_tracks.last.modified['n']).to eq(user.name)
223
+ end
224
+
225
+ it 'should undo field changes' do
226
+ name = user.name
227
+ user.update_attributes!(name: 'Aaron2', email: 'aaronsnewemail@randomemail.com')
228
+ user.history_tracks.last.undo! nil
229
+ expect(user.reload.name).to eq(name)
230
+ end
231
+
232
+ it 'should undo non-existing field changes' do
233
+ post = Post.create!(modifier: user, views: 100)
234
+ expect(post.reload.title).to be_nil
235
+ post.update_attributes!(title: 'Aaron2')
236
+ expect(post.reload.title).to eq('Aaron2')
237
+ post.history_tracks.last.undo! user
238
+ expect(post.reload.title).to be_nil
239
+ end
240
+
241
+ it 'should track array changes' do
242
+ aliases = user.aliases
243
+ user.update_attributes!(aliases: %w[bob joe])
244
+ expect(user.history_tracks.last.original['aliases']).to eq(aliases)
245
+ expect(user.history_tracks.last.modified['aliases']).to eq(user.aliases)
246
+ end
247
+
248
+ it 'should undo array changes' do
249
+ aliases = user.aliases
250
+ user.update_attributes!(aliases: %w[bob joe])
251
+ user.history_tracks.last.undo! nil
252
+ expect(user.reload.aliases).to eq(aliases)
253
+ end
254
+ end
255
+
256
+ describe '#tracked_changes' do
257
+ context 'create action' do
258
+ subject { tag.history_tracks.first.tracked_changes }
259
+ it 'consider all fields values as :to' do
260
+ expect(subject[:title]).to eq({ to: 'test' }.with_indifferent_access)
261
+ end
262
+ end
263
+ context 'destroy action' do
264
+ subject do
265
+ tag.destroy
266
+ tag.history_tracks.last.tracked_changes
267
+ end
268
+ it 'consider all fields values as :from' do
269
+ expect(subject[:title]).to eq({ from: 'test' }.with_indifferent_access)
270
+ end
271
+ end
272
+ context 'update action' do
273
+ subject { user.history_tracks.last.tracked_changes }
274
+ before do
275
+ user.update_attributes!(name: 'Aaron2', email: nil, country: '', city: nil, phone: '867-5309', aliases: ['', 'bill', 'james'])
276
+ end
277
+ it { is_expected.to be_a HashWithIndifferentAccess }
278
+ it 'should track changed field' do
279
+ expect(subject[:n]).to eq({ from: 'Aaron', to: 'Aaron2' }.with_indifferent_access)
280
+ end
281
+ it 'should track added field' do
282
+ expect(subject[:phone]).to eq({ to: '867-5309' }.with_indifferent_access)
283
+ end
284
+ it 'should track removed field' do
285
+ expect(subject[:city]).to eq({ from: 'Toronto' }.with_indifferent_access)
286
+ end
287
+ it 'should not consider blank as removed' do
288
+ expect(subject[:country]).to eq({ from: 'Canada', to: '' }.with_indifferent_access)
289
+ end
290
+ it 'should track changed array field' do
291
+ expect(subject[:aliases]).to eq({ from: ['bob'], to: ['', 'bill', 'james'] }.with_indifferent_access)
292
+ end
293
+ it 'should not track unmodified field' do
294
+ expect(subject[:address]).to be_nil
295
+ end
296
+ it 'should not track untracked fields' do
297
+ expect(subject[:email]).to be_nil
298
+ end
299
+ end
300
+ end
301
+
302
+ describe '#tracked_edits' do
303
+ context 'create action' do
304
+ subject { tag.history_tracks.first.tracked_edits }
305
+ it 'consider all edits as ;add' do
306
+ expect(subject[:add]).to eq({ title: 'test' }.with_indifferent_access)
307
+ end
308
+ end
309
+ context 'destroy action' do
310
+ subject do
311
+ tag.destroy
312
+ tag.history_tracks.last.tracked_edits
313
+ end
314
+ it 'consider all edits as ;remove' do
315
+ expect(subject[:remove]).to eq({ title: 'test' }.with_indifferent_access)
316
+ end
317
+ end
318
+ context 'update action' do
319
+ subject { user.history_tracks.last.tracked_edits }
320
+ before do
321
+ user.update_attributes!(name: 'Aaron2', email: nil, country: '', city: nil, phone: '867-5309', aliases: ['', 'bill', 'james'])
322
+ end
323
+ it { is_expected.to be_a HashWithIndifferentAccess }
324
+ it 'should track changed field' do
325
+ expect(subject[:modify]).to eq({ n: { from: 'Aaron', to: 'Aaron2' } }.with_indifferent_access)
326
+ end
327
+ it 'should track added field' do
328
+ expect(subject[:add]).to eq({ phone: '867-5309' }.with_indifferent_access)
329
+ end
330
+ it 'should track removed field and consider blank as removed' do
331
+ expect(subject[:remove]).to eq({ city: 'Toronto', country: 'Canada' }.with_indifferent_access)
332
+ end
333
+ it 'should track changed array field' do
334
+ expect(subject[:array]).to eq({ aliases: { remove: ['bob'], add: ['', 'bill', 'james'] } }.with_indifferent_access)
335
+ end
336
+ it 'should not track unmodified field' do
337
+ %w[add modify remove array].each do |edit|
338
+ expect(subject[edit][:address]).to be_nil
339
+ end
340
+ end
341
+ it 'should not track untracked fields' do
342
+ %w[add modify remove array].each do |edit|
343
+ expect(subject[edit][:email]).to be_nil
344
+ end
345
+ end
346
+ end
347
+ context 'with empty values' do
348
+ before do
349
+ allow(subject).to receive(:trackable_parent_class) { Tracker }
350
+ allow(Tracker).to receive(:tracked_embeds_many?) { false }
351
+ end
352
+ subject { Tracker.new }
353
+ it 'should skip empty values' do
354
+ allow(subject).to receive(:tracked_changes) { { name: { to: '', from: [] }, city: { to: 'Toronto', from: '' } } }
355
+ expect(subject.tracked_edits).to eq({ add: { city: 'Toronto' } }.with_indifferent_access)
356
+ end
357
+ end
358
+ end
359
+
360
+ describe 'on update non-embedded twice' do
361
+ it 'should assign version on post' do
362
+ expect(post.version).to eq(1)
363
+ post.update_attributes!(title: 'Test2')
364
+ post.update_attributes!(title: 'Test3')
365
+ expect(post.version).to eq(3)
366
+ end
367
+
368
+ it 'should create a history track if changed attributes match tracked attributes' do
369
+ post # Created
370
+ expect do
371
+ post.update_attributes!(title: 'Test2')
372
+ post.update_attributes!(title: 'Test3')
373
+ end.to change(Tracker, :count).by(2)
374
+ end
375
+
376
+ it 'should create a history track of version 2' do
377
+ post.update_attributes!(title: 'Test2')
378
+ post.update_attributes!(title: 'Test3')
379
+ expect(post.history_tracks.where(version: 2).first).not_to be_nil
380
+ end
381
+
382
+ it 'should assign modified fields' do
383
+ post.update_attributes!(title: 'Test2')
384
+ post.update_attributes!(title: 'Test3')
385
+ expect(post.history_tracks.where(version: 3).first.modified).to eq(
386
+ 'title' => 'Test3'
387
+ )
388
+ end
389
+
390
+ it 'should assign original fields' do
391
+ post.update_attributes!(title: 'Test2')
392
+ post.update_attributes!(title: 'Test3')
393
+ expect(post.history_tracks.where(version: 3).first.original).to eq(
394
+ 'title' => 'Test2'
395
+ )
396
+ end
397
+
398
+ it 'should assign modifier' do
399
+ post.update_attributes!(title: 'Another Test', modifier: another_user)
400
+ expect(post.history_tracks.last.modifier.id).to eq(another_user.id)
401
+ end
402
+ end
403
+
404
+ describe 'on update embedded 1..N (embeds_many)' do
405
+ it 'should assign version on comment' do
406
+ comment.update_attributes!(title: 'Test2')
407
+ expect(comment.version).to eq(2) # first track generated on creation
408
+ end
409
+
410
+ it 'should create a history track of version 2' do
411
+ comment.update_attributes!(title: 'Test2')
412
+ expect(comment.history_tracks.where(version: 2).first).not_to be_nil
413
+ end
414
+
415
+ it 'should assign modified fields' do
416
+ comment.update_attributes!(t: 'Test2')
417
+ expect(comment.history_tracks.where(version: 2).first.modified).to eq(
418
+ 't' => 'Test2'
419
+ )
420
+ end
421
+
422
+ it 'should assign original fields' do
423
+ comment.update_attributes!(title: 'Test2')
424
+ expect(comment.history_tracks.where(version: 2).first.original).to eq(
425
+ 't' => 'test'
426
+ )
427
+ end
428
+
429
+ it 'should be possible to undo from parent' do
430
+ comment.update_attributes!(title: 'Test 2')
431
+ user
432
+ post.history_tracks.last.undo!(user)
433
+ comment.reload
434
+ expect(comment.title).to eq('test')
435
+ end
436
+
437
+ it 'should assign modifier' do
438
+ post.update_attributes!(title: 'Another Test', modifier: another_user)
439
+ expect(post.history_tracks.last.modifier.id).to eq(another_user.id)
440
+ end
441
+ end
442
+
443
+ describe 'on update embedded 1..1 (embeds_one)' do
444
+ let(:section) { Section.new(title: 'Technology', modifier: user) }
445
+
446
+ before(:each) do
447
+ post.section = section
448
+ post.modifier = user
449
+ post.save!
450
+ post.reload
451
+ post.section
452
+ end
453
+
454
+ it 'should assign version on create section' do
455
+ expect(section.version).to eq(1)
456
+ end
457
+
458
+ it 'should assign version on section' do
459
+ section.update_attributes!(title: 'Technology 2')
460
+ expect(section.version).to eq(2) # first track generated on creation
461
+ end
462
+
463
+ it 'should create a history track of version 2' do
464
+ section.update_attributes!(title: 'Technology 2')
465
+ expect(section.history_tracks.where(version: 2).first).not_to be_nil
466
+ end
467
+
468
+ it 'should assign modified fields' do
469
+ section.update_attributes!(title: 'Technology 2')
470
+ expect(section.history_tracks.where(version: 2).first.modified).to eq(
471
+ 't' => 'Technology 2'
472
+ )
473
+ end
474
+
475
+ it 'should assign original fields' do
476
+ section.update_attributes!(title: 'Technology 2')
477
+ expect(section.history_tracks.where(version: 2).first.original).to eq(
478
+ 't' => 'Technology'
479
+ )
480
+ end
481
+
482
+ it 'should be possible to undo from parent' do
483
+ section.update_attributes!(title: 'Technology 2')
484
+ post.history_tracks.last.undo!(user)
485
+ section.reload
486
+ expect(section.title).to eq('Technology')
487
+ end
488
+
489
+ it 'should assign modifier' do
490
+ section.update_attributes!(title: 'Business', modifier: another_user)
491
+ expect(post.history_tracks.last.modifier.id).to eq(another_user.id)
492
+ end
493
+ end
494
+
495
+ describe 'on destroy embedded' do
496
+ it 'should be possible to re-create destroyed embedded' do
497
+ comment.destroy
498
+ comment.history_tracks.last.undo!(user)
499
+ post.reload
500
+ expect(post.comments.first.title).to eq('test')
501
+ end
502
+
503
+ it 'should be possible to re-create destroyed embedded from parent' do
504
+ comment.destroy
505
+ post.history_tracks.last.undo!(user)
506
+ post.reload
507
+ expect(post.comments.first.title).to eq('test')
508
+ end
509
+
510
+ it 'should be possible to destroy after re-create embedded from parent' do
511
+ comment.destroy
512
+ post.history_tracks[-1].undo!(user)
513
+ post.history_tracks[-1].undo!(user)
514
+ post.reload
515
+ expect(post.comments.count).to eq(0)
516
+ end
517
+
518
+ it 'should be possible to create with redo after undo create embedded from parent' do
519
+ comment # initialize
520
+ post.comments.create!(title: 'The second one', modifier: user)
521
+ track = post.history_tracks[2]
522
+ expect(post.reload.comments.count).to eq 2
523
+ track.undo!(user)
524
+ expect(post.reload.comments.count).to eq 1
525
+ track.redo!(user)
526
+ expect(post.reload.comments.count).to eq 2
527
+ end
528
+ end
529
+
530
+ describe 'embedded with cascading callbacks' do
531
+ let(:tag_foo) { post.tags.create!(title: 'foo', updated_by: user) }
532
+ let(:tag_bar) { post.tags.create!(title: 'bar', updated_by: user) }
533
+
534
+ it 'should allow an update through the parent model' do
535
+ update_hash = { 'post' => { 'tags_attributes' => { '1234' => { 'id' => tag_bar.id, 'title' => 'baz' } } } }
536
+ post.update_attributes!(update_hash['post'])
537
+ expect(post.tags.last.title).to eq('baz')
538
+ end
539
+
540
+ it 'should be possible to destroy through parent model using canoncial _destroy macro' do
541
+ tag_foo
542
+ tag_bar # initialize
543
+ expect(post.tags.count).to eq(2)
544
+ update_hash = { 'post' => { 'tags_attributes' => { '1234' => { 'id' => tag_bar.id, 'title' => 'baz', '_destroy' => 'true' } } } }
545
+ post.update_attributes!(update_hash['post'])
546
+ expect(post.tags.count).to eq(1)
547
+ expect(post.history_tracks.to_a.last.action).to eq('destroy')
548
+ end
549
+
550
+ it 'should write relationship name for association_chain hiearchy instead of class name when using _destroy macro' do
551
+ update_hash = { 'tags_attributes' => { '1234' => { 'id' => tag_foo.id, '_destroy' => '1' } } }
552
+ post.update_attributes!(update_hash)
553
+
554
+ # historically this would have evaluated to 'Tags' and an error would be thrown
555
+ # on any call that walked up the association_chain, e.g. 'trackable'
556
+ expect(tag_foo.history_tracks.last.association_chain.last['name']).to eq('tags')
557
+ expect { tag_foo.history_tracks.last.trackable }.not_to raise_error
558
+ end
559
+ end
560
+
561
+ describe 'non-embedded' do
562
+ it 'should undo changes' do
563
+ post.update_attributes!(title: 'Test2')
564
+ post.history_tracks.where(version: 2).last.undo!(user)
565
+ post.reload
566
+ expect(post.title).to eq('Test')
567
+ end
568
+
569
+ it 'should undo destruction' do
570
+ post.destroy
571
+ post.history_tracks.where(version: 2).last.undo!(user)
572
+ expect(Post.find(post.id).title).to eq('Test')
573
+ end
574
+
575
+ it 'should create a new history track after undo' do
576
+ comment # initialize
577
+ post.update_attributes!(title: 'Test2')
578
+ post.history_tracks.last.undo!(user)
579
+ post.reload
580
+ expect(post.history_tracks.count).to eq(4)
581
+ end
582
+
583
+ it 'should assign user as the modifier of the newly created history track' do
584
+ post.update_attributes!(title: 'Test2')
585
+ post.history_tracks.where(version: 2).last.undo!(user)
586
+ post.reload
587
+ expect(post.history_tracks.where(version: 2).last.modifier.id).to eq(user.id)
588
+ end
589
+
590
+ it 'should stay the same after undo and redo' do
591
+ post.update_attributes!(title: 'Test2')
592
+ track = post.history_tracks.last
593
+ track.undo!(user)
594
+ track.redo!(user)
595
+ post2 = Post.where(_id: post.id).first
596
+
597
+ expect(post.title).to eq(post2.title)
598
+ end
599
+
600
+ it 'should be destroyed after undo and redo' do
601
+ post.destroy
602
+ track = post.history_tracks.where(version: 2).last
603
+ track.undo!(user)
604
+ track.redo!(user)
605
+ expect(Post.where(_id: post.id).first).to be_nil
606
+ end
607
+ end
608
+
609
+ describe 'embedded' do
610
+ it 'should undo changes' do
611
+ comment.update_attributes!(title: 'Test2')
612
+ comment.history_tracks.where(version: 2).first.undo!(user)
613
+ comment.reload
614
+ expect(comment.title).to eq('test')
615
+ end
616
+
617
+ it 'should create a new history track after undo' do
618
+ comment.update_attributes!(title: 'Test2')
619
+ comment.history_tracks.where(version: 2).first.undo!(user)
620
+ comment.reload
621
+ expect(comment.history_tracks.count).to eq(3)
622
+ end
623
+
624
+ it 'should assign user as the modifier of the newly created history track' do
625
+ comment.update_attributes!(title: 'Test2')
626
+ comment.history_tracks.where(version: 2).first.undo!(user)
627
+ comment.reload
628
+ expect(comment.history_tracks.where(version: 3).first.modifier.id).to eq(user.id)
629
+ end
630
+
631
+ it 'should stay the same after undo and redo' do
632
+ comment.update_attributes!(title: 'Test2')
633
+ track = comment.history_tracks.where(version: 2).first
634
+ track.undo!(user)
635
+ track.redo!(user)
636
+ comment.reload
637
+ expect(comment.title).to eq('Test2')
638
+ end
639
+ end
640
+
641
+ describe 'trackables' do
642
+ before :each do
643
+ comment.update_attributes!(title: 'Test2') # version == 2
644
+ comment.update_attributes!(title: 'Test3') # version == 3
645
+ comment.update_attributes!(title: 'Test4') # version == 4
646
+ end
647
+
648
+ describe 'undo' do
649
+ { 'undo' => [nil], 'undo!' => [nil, :reload] }.each do |test_method, methods|
650
+ methods.each do |method|
651
+ context (method || 'instance').to_s do
652
+ it 'recognizes :from, :to options' do
653
+ comment.send test_method, user, from: 4, to: 2
654
+ comment.send(method) if method
655
+ expect(comment.title).to eq('test')
656
+ end
657
+
658
+ it 'recognizes parameter as version number' do
659
+ comment.send test_method, user, 3
660
+ comment.send(method) if method
661
+ expect(comment.title).to eq('Test2')
662
+ end
663
+
664
+ it 'should undo last version when no parameter is specified' do
665
+ comment.send test_method, user
666
+ comment.send(method) if method
667
+ expect(comment.title).to eq('Test3')
668
+ end
669
+
670
+ it 'recognizes :last options' do
671
+ comment.send test_method, user, last: 2
672
+ comment.send(method) if method
673
+ expect(comment.title).to eq('Test2')
674
+ end
675
+
676
+ if Mongoid::Compatibility::Version.mongoid3?
677
+ context 'protected attributes' do
678
+ before :each do
679
+ Comment.attr_accessible(nil)
680
+ end
681
+
682
+ after :each do
683
+ Comment.attr_protected(nil)
684
+ end
685
+
686
+ it 'should undo last version when no parameter is specified on protected attributes' do
687
+ comment.send test_method, user
688
+ comment.send(method) if method
689
+ expect(comment.title).to eq('Test3')
690
+ end
691
+
692
+ it 'recognizes :last options on model with protected attributes' do
693
+ comment.send test_method, user, last: 2
694
+ comment.send(method) if method
695
+ expect(comment.title).to eq('Test2')
696
+ end
697
+ end
698
+ end
699
+ end
700
+ end
701
+ end
702
+ end
703
+
704
+ describe 'redo' do
705
+ [nil, :reload].each do |method|
706
+ context (method || 'instance').to_s do
707
+ before :each do
708
+ comment.update_attributes!(title: 'Test5')
709
+ end
710
+
711
+ it 'should recognize :from, :to options' do
712
+ comment.redo! user, from: 2, to: 4
713
+ comment.send(method) if method
714
+ expect(comment.title).to eq('Test4')
715
+ end
716
+
717
+ it 'should recognize parameter as version number' do
718
+ comment.redo! user, 2
719
+ comment.send(method) if method
720
+ expect(comment.title).to eq('Test2')
721
+ end
722
+
723
+ it 'should redo last version when no parameter is specified' do
724
+ comment.redo! user
725
+ comment.send(method) if method
726
+ expect(comment.title).to eq('Test5')
727
+ end
728
+
729
+ it 'should recognize :last options' do
730
+ comment.redo! user, last: 1
731
+ comment.send(method) if method
732
+ expect(comment.title).to eq('Test5')
733
+ end
734
+
735
+ if Mongoid::Compatibility::Version.mongoid3?
736
+ context 'protected attributes' do
737
+ before :each do
738
+ Comment.attr_accessible(nil)
739
+ end
740
+
741
+ after :each do
742
+ Comment.attr_protected(nil)
743
+ end
744
+
745
+ it 'should recognize parameter as version number' do
746
+ comment.redo! user, 2
747
+ comment.send(method) if method
748
+ expect(comment.title).to eq('Test2')
749
+ end
750
+
751
+ it 'should recognize :from, :to options' do
752
+ comment.redo! user, from: 2, to: 4
753
+ comment.send(method) if method
754
+ expect(comment.title).to eq('Test4')
755
+ end
756
+ end
757
+ end
758
+ end
759
+ end
760
+ end
761
+ end
762
+
763
+ describe 'embedded with a polymorphic trackable' do
764
+ let(:foo) { Foo.new(title: 'a title', body: 'a body', modifier: user) }
765
+ before :each do
766
+ post.comments << foo
767
+ post.save!
768
+ end
769
+ it 'should assign interface name in association chain' do
770
+ foo.update_attribute(:body, 'a changed body')
771
+ expected_root = { 'name' => 'Post', 'id' => post.id }
772
+ expected_node = { 'name' => 'coms', 'id' => foo.id }
773
+ expect(foo.history_tracks.first.association_chain).to eq([expected_root, expected_node])
774
+ end
775
+ end
776
+
777
+ describe '#trackable_parent_class' do
778
+ context 'a non-embedded model' do
779
+ it 'should return the trackable parent class' do
780
+ expect(tag.history_tracks.first.trackable_parent_class).to eq(Tag)
781
+ end
782
+ it 'should return the parent class even if the trackable is deleted' do
783
+ tracker = tag.history_tracks.first
784
+ tag.destroy
785
+ expect(tracker.trackable_parent_class).to eq(Tag)
786
+ end
787
+ end
788
+ context 'an embedded model' do
789
+ it 'should return the trackable parent class' do
790
+ comment.update_attributes!(title: 'Foo')
791
+ expect(comment.history_tracks.first.trackable_parent_class).to eq(Post)
792
+ end
793
+ it 'should return the parent class even if the trackable is deleted' do
794
+ tracker = comment.history_tracks.first
795
+ comment.destroy
796
+ expect(tracker.trackable_parent_class).to eq(Post)
797
+ end
798
+ end
799
+ end
800
+
801
+ describe 'when default scope is present' do
802
+ before :each do
803
+ class Post
804
+ default_scope -> { where(title: nil) }
805
+ end
806
+ class Comment
807
+ default_scope -> { where(title: nil) }
808
+ end
809
+ class User
810
+ default_scope -> { where(name: nil) }
811
+ end
812
+ class Tag
813
+ default_scope -> { where(title: nil) }
814
+ end
815
+ end
816
+
817
+ describe 'post' do
818
+ it 'should correctly undo and redo' do
819
+ post.update_attributes!(title: 'a new title')
820
+ track = post.history_tracks.last
821
+ track.undo! user
822
+ expect(post.reload.title).to eq('Test')
823
+ track.redo! user
824
+ expect(post.reload.title).to eq('a new title')
825
+ end
826
+
827
+ it 'should stay the same after undo and redo' do
828
+ post.update_attributes!(title: 'testing')
829
+ track = post.history_tracks.last
830
+ track.undo! user
831
+ track.redo! user
832
+ expect(post.reload.title).to eq('testing')
833
+ end
834
+ end
835
+ describe 'comment' do
836
+ it 'should correctly undo and redo' do
837
+ comment.update_attributes!(title: 'a new title')
838
+ track = comment.history_tracks.last
839
+ track.undo! user
840
+ expect(comment.reload.title).to eq('test')
841
+ track.redo! user
842
+ expect(comment.reload.title).to eq('a new title')
843
+ end
844
+
845
+ it 'should stay the same after undo and redo' do
846
+ comment.update_attributes!(title: 'testing')
847
+ track = comment.history_tracks.last
848
+ track.undo! user
849
+ track.redo! user
850
+ expect(comment.reload.title).to eq('testing')
851
+ end
852
+ end
853
+ describe 'user' do
854
+ it 'should correctly undo and redo' do
855
+ user.update_attributes!(name: 'a new name')
856
+ track = user.history_tracks.last
857
+ track.undo! user
858
+ expect(user.reload.name).to eq('Aaron')
859
+ track.redo! user
860
+ expect(user.reload.name).to eq('a new name')
861
+ end
862
+
863
+ it 'should stay the same after undo and redo' do
864
+ user.update_attributes!(name: 'testing')
865
+ track = user.history_tracks.last
866
+ track.undo! user
867
+ track.redo! user
868
+ expect(user.reload.name).to eq('testing')
869
+ end
870
+ end
871
+ describe 'tag' do
872
+ it 'should correctly undo and redo' do
873
+ tag.update_attributes!(title: 'a new title')
874
+ track = tag.history_tracks.last
875
+ track.undo! user
876
+ expect(tag.reload.title).to eq('test')
877
+ track.redo! user
878
+ expect(tag.reload.title).to eq('a new title')
879
+ end
880
+
881
+ it 'should stay the same after undo and redo' do
882
+ tag.update_attributes!(title: 'testing')
883
+ track = tag.history_tracks.last
884
+ track.undo! user
885
+ track.redo! user
886
+ expect(tag.reload.title).to eq('testing')
887
+ end
888
+ end
889
+ end
890
+
891
+ describe 'overriden changes_method with additional fields' do
892
+ before :each do
893
+ class OverriddenChangesMethod
894
+ include Mongoid::Document
895
+ include Mongoid::History::Trackable
896
+
897
+ track_history on: [:foo], changes_method: :my_changes
898
+
899
+ def my_changes
900
+ { foo: %w[bar baz] }
901
+ end
902
+ end
903
+ end
904
+
905
+ after :each do
906
+ Object.send(:remove_const, :OverriddenChangesMethod)
907
+ end
908
+
909
+ it 'should add foo to the changes history' do
910
+ o = OverriddenChangesMethod.create(modifier: user)
911
+ o.save!
912
+ track = o.history_tracks.last
913
+ expect(track.modified).to eq('foo' => 'baz')
914
+ expect(track.original).to eq('foo' => 'bar')
915
+ end
916
+ end
917
+
918
+ describe 'localized fields' do
919
+ before :each do
920
+ class Sausage
921
+ include Mongoid::Document
922
+ include Mongoid::History::Trackable
923
+
924
+ field :flavour, localize: true
925
+ track_history on: [:flavour], track_destroy: true, modifier_field_optional: true
926
+ end
927
+ end
928
+
929
+ after :each do
930
+ Object.send(:remove_const, :Sausage)
931
+ end
932
+
933
+ it 'should correctly undo and redo' do
934
+ pending unless Sausage.respond_to?(:localized_fields)
935
+
936
+ sausage = Sausage.create!(flavour_translations: { 'en' => 'Apple', 'nl' => 'Appel' }, modifier: user)
937
+ sausage.update_attributes!(flavour: 'Guinness')
938
+
939
+ track = sausage.history_tracks.last
940
+
941
+ track.undo! user
942
+ expect(sausage.reload.flavour).to eq('Apple')
943
+
944
+ track.redo! user
945
+ expect(sausage.reload.flavour).to eq('Guinness')
946
+
947
+ sausage.destroy
948
+ expect(sausage.history_tracks.last.action).to eq('destroy')
949
+ sausage.history_tracks.last.undo! user
950
+ expect(sausage.reload.flavour).to eq('Guinness')
951
+ end
952
+ end
953
+
954
+ describe 'changing collection' do
955
+ before :each do
956
+ class Fish
957
+ include Mongoid::Document
958
+ include Mongoid::History::Trackable
959
+
960
+ track_history on: [:species], modifier_field_optional: true
961
+ store_in collection: :animals
962
+
963
+ field :species
964
+ end
965
+ end
966
+
967
+ after :each do
968
+ Object.send(:remove_const, :Fish)
969
+ end
970
+
971
+ it 'should track history' do
972
+ Fish.new.save!
973
+ end
974
+ end
975
+ end
976
+ end