mongoid-history 0.8.0 → 0.8.5

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