paper_trail 4.0.0 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (158) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CONTRIBUTING.md +105 -0
  3. data/.github/ISSUE_TEMPLATE.md +13 -0
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +100 -0
  6. data/.rubocop_todo.yml +14 -0
  7. data/.travis.yml +11 -10
  8. data/Appraisals +37 -0
  9. data/CHANGELOG.md +173 -8
  10. data/Gemfile +1 -1
  11. data/README.md +641 -470
  12. data/Rakefile +19 -19
  13. data/doc/bug_report_template.rb +71 -0
  14. data/doc/warning_about_not_setting_whodunnit.md +32 -0
  15. data/gemfiles/ar3.gemfile +18 -0
  16. data/gemfiles/ar4.gemfile +7 -0
  17. data/gemfiles/ar5.gemfile +13 -0
  18. data/lib/generators/paper_trail/install_generator.rb +26 -18
  19. data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb +3 -1
  20. data/lib/generators/paper_trail/templates/add_transaction_id_column_to_versions.rb +2 -0
  21. data/lib/generators/paper_trail/templates/create_version_associations.rb +9 -4
  22. data/lib/generators/paper_trail/templates/create_versions.rb +53 -5
  23. data/lib/paper_trail/attribute_serializers/README.md +10 -0
  24. data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +58 -0
  25. data/lib/paper_trail/attribute_serializers/legacy_active_record_shim.rb +48 -0
  26. data/lib/paper_trail/attribute_serializers/object_attribute.rb +39 -0
  27. data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +42 -0
  28. data/lib/paper_trail/cleaner.rb +41 -18
  29. data/lib/paper_trail/config.rb +42 -26
  30. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +5 -1
  31. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +6 -2
  32. data/lib/paper_trail/frameworks/active_record.rb +2 -2
  33. data/lib/paper_trail/frameworks/cucumber.rb +1 -0
  34. data/lib/paper_trail/frameworks/rails/controller.rb +50 -14
  35. data/lib/paper_trail/frameworks/rails/engine.rb +6 -1
  36. data/lib/paper_trail/frameworks/rails.rb +2 -7
  37. data/lib/paper_trail/frameworks/rspec/helpers.rb +3 -1
  38. data/lib/paper_trail/frameworks/rspec.rb +5 -5
  39. data/lib/paper_trail/frameworks/sinatra.rb +8 -5
  40. data/lib/paper_trail/has_paper_trail.rb +381 -221
  41. data/lib/paper_trail/record_history.rb +57 -0
  42. data/lib/paper_trail/reifier.rb +450 -0
  43. data/lib/paper_trail/serializers/json.rb +7 -7
  44. data/lib/paper_trail/serializers/yaml.rb +31 -12
  45. data/lib/paper_trail/version_association_concern.rb +6 -2
  46. data/lib/paper_trail/version_concern.rb +200 -287
  47. data/lib/paper_trail/version_number.rb +6 -9
  48. data/lib/paper_trail.rb +169 -137
  49. data/paper_trail.gemspec +41 -43
  50. data/spec/generators/install_generator_spec.rb +24 -25
  51. data/spec/generators/paper_trail/templates/create_versions_spec.rb +51 -0
  52. data/spec/models/animal_spec.rb +23 -6
  53. data/spec/models/boolit_spec.rb +8 -8
  54. data/spec/models/callback_modifier_spec.rb +96 -0
  55. data/spec/models/car_spec.rb +13 -0
  56. data/spec/models/fluxor_spec.rb +3 -3
  57. data/spec/models/gadget_spec.rb +19 -19
  58. data/spec/models/joined_version_spec.rb +3 -3
  59. data/spec/models/json_version_spec.rb +50 -28
  60. data/spec/models/kitchen/banana_spec.rb +3 -3
  61. data/spec/models/not_on_update_spec.rb +7 -4
  62. data/spec/models/post_with_status_spec.rb +13 -3
  63. data/spec/models/skipper_spec.rb +40 -11
  64. data/spec/models/thing_spec.rb +4 -4
  65. data/spec/models/truck_spec.rb +5 -0
  66. data/spec/models/vehicle_spec.rb +5 -0
  67. data/spec/models/version_spec.rb +103 -59
  68. data/spec/models/widget_spec.rb +86 -55
  69. data/spec/modules/paper_trail_spec.rb +2 -2
  70. data/spec/modules/version_concern_spec.rb +11 -12
  71. data/spec/modules/version_number_spec.rb +3 -4
  72. data/spec/paper_trail/config_spec.rb +33 -0
  73. data/spec/paper_trail_spec.rb +16 -14
  74. data/spec/rails_helper.rb +10 -9
  75. data/spec/requests/articles_spec.rb +11 -7
  76. data/spec/spec_helper.rb +42 -17
  77. data/spec/support/alt_db_init.rb +8 -13
  78. data/test/custom_json_serializer.rb +3 -3
  79. data/test/dummy/Rakefile +2 -2
  80. data/test/dummy/app/controllers/application_controller.rb +21 -8
  81. data/test/dummy/app/controllers/articles_controller.rb +11 -8
  82. data/test/dummy/app/controllers/widgets_controller.rb +13 -12
  83. data/test/dummy/app/models/animal.rb +1 -1
  84. data/test/dummy/app/models/article.rb +19 -11
  85. data/test/dummy/app/models/authorship.rb +1 -1
  86. data/test/dummy/app/models/bar_habtm.rb +4 -0
  87. data/test/dummy/app/models/book.rb +4 -4
  88. data/test/dummy/app/models/boolit.rb +1 -1
  89. data/test/dummy/app/models/callback_modifier.rb +45 -0
  90. data/test/dummy/app/models/car.rb +3 -0
  91. data/test/dummy/app/models/chapter.rb +9 -0
  92. data/test/dummy/app/models/citation.rb +5 -0
  93. data/test/dummy/app/models/customer.rb +1 -1
  94. data/test/dummy/app/models/document.rb +2 -2
  95. data/test/dummy/app/models/editor.rb +1 -1
  96. data/test/dummy/app/models/foo_habtm.rb +5 -0
  97. data/test/dummy/app/models/fruit.rb +2 -2
  98. data/test/dummy/app/models/gadget.rb +1 -1
  99. data/test/dummy/app/models/kitchen/banana.rb +1 -1
  100. data/test/dummy/app/models/legacy_widget.rb +2 -2
  101. data/test/dummy/app/models/line_item.rb +1 -1
  102. data/test/dummy/app/models/not_on_update.rb +1 -1
  103. data/test/dummy/app/models/paragraph.rb +5 -0
  104. data/test/dummy/app/models/person.rb +6 -6
  105. data/test/dummy/app/models/post.rb +1 -1
  106. data/test/dummy/app/models/post_with_status.rb +1 -1
  107. data/test/dummy/app/models/quotation.rb +5 -0
  108. data/test/dummy/app/models/section.rb +6 -0
  109. data/test/dummy/app/models/skipper.rb +2 -2
  110. data/test/dummy/app/models/song.rb +13 -4
  111. data/test/dummy/app/models/thing.rb +2 -2
  112. data/test/dummy/app/models/translation.rb +2 -2
  113. data/test/dummy/app/models/truck.rb +4 -0
  114. data/test/dummy/app/models/vehicle.rb +4 -0
  115. data/test/dummy/app/models/whatchamajigger.rb +1 -1
  116. data/test/dummy/app/models/widget.rb +7 -6
  117. data/test/dummy/app/versions/joined_version.rb +4 -3
  118. data/test/dummy/app/versions/json_version.rb +1 -1
  119. data/test/dummy/app/versions/kitchen/banana_version.rb +1 -1
  120. data/test/dummy/app/versions/post_version.rb +2 -2
  121. data/test/dummy/config/application.rb +20 -9
  122. data/test/dummy/config/boot.rb +5 -5
  123. data/test/dummy/config/database.postgres.yml +1 -1
  124. data/test/dummy/config/environment.rb +1 -1
  125. data/test/dummy/config/environments/development.rb +4 -3
  126. data/test/dummy/config/environments/production.rb +3 -2
  127. data/test/dummy/config/environments/test.rb +15 -5
  128. data/test/dummy/config/initializers/backtrace_silencers.rb +4 -2
  129. data/test/dummy/config/initializers/paper_trail.rb +4 -3
  130. data/test/dummy/config/initializers/secret_token.rb +3 -1
  131. data/test/dummy/config/initializers/session_store.rb +1 -1
  132. data/test/dummy/config/routes.rb +2 -2
  133. data/test/dummy/config.ru +1 -1
  134. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +148 -68
  135. data/test/dummy/db/schema.rb +119 -31
  136. data/test/dummy/script/rails +6 -4
  137. data/test/functional/controller_test.rb +34 -35
  138. data/test/functional/enabled_for_controller_test.rb +6 -7
  139. data/test/functional/modular_sinatra_test.rb +43 -38
  140. data/test/functional/sinatra_test.rb +49 -40
  141. data/test/functional/thread_safety_test.rb +4 -6
  142. data/test/paper_trail_test.rb +15 -14
  143. data/test/test_helper.rb +78 -18
  144. data/test/time_travel_helper.rb +1 -15
  145. data/test/unit/associations_test.rb +1016 -0
  146. data/test/unit/cleaner_test.rb +66 -60
  147. data/test/unit/inheritance_column_test.rb +19 -19
  148. data/test/unit/model_test.rb +646 -1071
  149. data/test/unit/protected_attrs_test.rb +19 -14
  150. data/test/unit/serializer_test.rb +44 -43
  151. data/test/unit/serializers/json_test.rb +28 -21
  152. data/test/unit/serializers/mixin_json_test.rb +15 -14
  153. data/test/unit/serializers/mixin_yaml_test.rb +20 -16
  154. data/test/unit/serializers/yaml_test.rb +16 -14
  155. data/test/unit/timestamp_test.rb +10 -12
  156. data/test/unit/version_test.rb +88 -70
  157. metadata +166 -72
  158. data/gemfiles/3.0.gemfile +0 -52
@@ -0,0 +1,1016 @@
1
+ require "test_helper"
2
+ require "time_travel_helper"
3
+
4
+ class AssociationsTest < ActiveSupport::TestCase
5
+ CHAPTER_NAMES = [
6
+ "Down the Rabbit-Hole",
7
+ "The Pool of Tears",
8
+ "A Caucus-Race and a Long Tale",
9
+ "The Rabbit Sends in a Little Bill",
10
+ "Advice from a Caterpillar",
11
+ "Pig and Pepper",
12
+ "A Mad Tea-Party",
13
+ "The Queen's Croquet-Ground",
14
+ "The Mock Turtle's Story",
15
+ "The Lobster Quadrille",
16
+ "Who Stole the Tarts?",
17
+ "Alice's Evidence"
18
+ ].freeze
19
+
20
+ # These would have been done in test_helper.rb if using_mysql? is true
21
+ unless using_mysql?
22
+ if respond_to? :use_transactional_tests=
23
+ self.use_transactional_tests = false
24
+ else
25
+ self.use_transactional_fixtures = false
26
+ end
27
+ setup { DatabaseCleaner.start }
28
+ end
29
+
30
+ teardown do
31
+ Timecop.return
32
+ # This would have been done in test_helper.rb if using_mysql? is true
33
+ DatabaseCleaner.clean unless using_mysql?
34
+ end
35
+
36
+ context "a has_one association" do
37
+ setup { @widget = Widget.create name: "widget_0" }
38
+
39
+ context "before the associated was created" do
40
+ setup do
41
+ @widget.update_attributes name: "widget_1"
42
+ @wotsit = @widget.create_wotsit name: "wotsit_0"
43
+ end
44
+
45
+ context "when reified" do
46
+ setup { @widget_0 = @widget.versions.last.reify(has_one: true) }
47
+
48
+ should "see the associated as it was at the time" do
49
+ assert_nil @widget_0.wotsit
50
+ end
51
+
52
+ should "not persist changes to the live association" do
53
+ assert_equal @wotsit, @widget.reload.wotsit
54
+ end
55
+ end
56
+ end
57
+
58
+ context "where the association is created between model versions" do
59
+ setup do
60
+ @wotsit = @widget.create_wotsit name: "wotsit_0"
61
+ Timecop.travel 1.second.since
62
+ @widget.update_attributes name: "widget_1"
63
+ end
64
+
65
+ context "when reified" do
66
+ setup { @widget_0 = @widget.versions.last.reify(has_one: true) }
67
+
68
+ should "see the associated as it was at the time" do
69
+ assert_equal "wotsit_0", @widget_0.wotsit.name
70
+ end
71
+
72
+ should "not persist changes to the live association" do
73
+ assert_equal @wotsit, @widget.reload.wotsit
74
+ end
75
+ end
76
+
77
+ context "and then the associated is updated between model versions" do
78
+ setup do
79
+ @wotsit.update_attributes name: "wotsit_1"
80
+ @wotsit.update_attributes name: "wotsit_2"
81
+ Timecop.travel 1.second.since
82
+ @widget.update_attributes name: "widget_2"
83
+ @wotsit.update_attributes name: "wotsit_3"
84
+ end
85
+
86
+ context "when reified" do
87
+ setup { @widget_1 = @widget.versions.last.reify(has_one: true) }
88
+
89
+ should "see the associated as it was at the time" do
90
+ assert_equal "wotsit_2", @widget_1.wotsit.name
91
+ end
92
+
93
+ should "not persist changes to the live association" do
94
+ assert_equal "wotsit_3", @widget.reload.wotsit.name
95
+ end
96
+ end
97
+
98
+ context "when reified opting out of has_one reification" do
99
+ setup { @widget_1 = @widget.versions.last.reify(has_one: false) }
100
+
101
+ should "see the associated as it is live" do
102
+ assert_equal "wotsit_3", @widget_1.wotsit.name
103
+ end
104
+ end
105
+ end
106
+
107
+ context "and then the associated is destroyed" do
108
+ setup do
109
+ @wotsit.destroy
110
+ end
111
+
112
+ context "when reify" do
113
+ setup { @widget_1 = @widget.versions.last.reify(has_one: true) }
114
+
115
+ should "see the associated as it was at the time" do
116
+ assert_equal @wotsit, @widget_1.wotsit
117
+ end
118
+
119
+ should "not persist changes to the live association" do
120
+ assert_nil @widget.reload.wotsit
121
+ end
122
+ end
123
+
124
+ context "and then the model is updated" do
125
+ setup do
126
+ Timecop.travel 1.second.since
127
+ @widget.update_attributes name: "widget_3"
128
+ end
129
+
130
+ context "when reified" do
131
+ setup { @widget_2 = @widget.versions.last.reify(has_one: true) }
132
+
133
+ should "see the associated as it was at the time" do
134
+ assert_nil @widget_2.wotsit
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ context "a has_many association" do
143
+ setup { @customer = Customer.create name: "customer_0" }
144
+
145
+ context "updated before the associated was created" do
146
+ setup do
147
+ @customer.update_attributes! name: "customer_1"
148
+ @customer.orders.create! order_date: Date.today
149
+ end
150
+
151
+ context "when reified" do
152
+ setup { @customer_0 = @customer.versions.last.reify(has_many: true) }
153
+
154
+ should "see the associated as it was at the time" do
155
+ assert_equal [], @customer_0.orders
156
+ end
157
+
158
+ should "not persist changes to the live association" do
159
+ assert_not_equal [], @customer.orders.reload
160
+ end
161
+ end
162
+
163
+ context "when reified with option mark_for_destruction" do
164
+ should "mark the associated for destruction" do
165
+ @customer_0 = @customer.versions.last.reify(
166
+ has_many: true,
167
+ mark_for_destruction: true
168
+ )
169
+ assert_equal [true], @customer_0.orders.map(&:marked_for_destruction?)
170
+ end
171
+ end
172
+ end
173
+
174
+ context "where the association is created between model versions" do
175
+ setup do
176
+ @order = @customer.orders.create! order_date: "order_date_0"
177
+ Timecop.travel 1.second.since
178
+ @customer.update_attributes name: "customer_1"
179
+ end
180
+
181
+ context "when reified" do
182
+ setup { @customer_0 = @customer.versions.last.reify(has_many: true) }
183
+
184
+ should "see the associated as it was at the time" do
185
+ assert_equal ["order_date_0"], @customer_0.orders.map(&:order_date)
186
+ end
187
+ end
188
+
189
+ context "and then a nested has_many association is created" do
190
+ setup do
191
+ @order.line_items.create! product: "product_0"
192
+ end
193
+
194
+ context "when reified" do
195
+ setup { @customer_0 = @customer.versions.last.reify(has_many: true) }
196
+
197
+ should "see the live version of the nested association" do
198
+ assert_equal ["product_0"], @customer_0.orders.first.line_items.map(&:product)
199
+ end
200
+ end
201
+ end
202
+
203
+ context "and then the associated is updated between model versions" do
204
+ setup do
205
+ @order.update_attributes order_date: "order_date_1"
206
+ @order.update_attributes order_date: "order_date_2"
207
+ Timecop.travel 1.second.since
208
+ @customer.update_attributes name: "customer_2"
209
+ @order.update_attributes order_date: "order_date_3"
210
+ end
211
+
212
+ context "when reified" do
213
+ setup { @customer_1 = @customer.versions.last.reify(has_many: true) }
214
+
215
+ should "see the associated as it was at the time" do
216
+ assert_equal ["order_date_2"], @customer_1.orders.map(&:order_date)
217
+ end
218
+
219
+ should "not persist changes to the live association" do
220
+ assert_equal ["order_date_3"], @customer.orders.reload.map(&:order_date)
221
+ end
222
+ end
223
+
224
+ context "when reified opting out of has_many reification" do
225
+ setup { @customer_1 = @customer.versions.last.reify(has_many: false) }
226
+
227
+ should "see the associated as it is live" do
228
+ assert_equal ["order_date_3"], @customer_1.orders.map(&:order_date)
229
+ end
230
+ end
231
+
232
+ context "and then the associated is destroyed" do
233
+ setup do
234
+ @order.destroy
235
+ end
236
+
237
+ context "when reified" do
238
+ setup { @customer_1 = @customer.versions.last.reify(has_many: true) }
239
+
240
+ should "see the associated as it was at the time" do
241
+ assert_equal ["order_date_2"], @customer_1.orders.map(&:order_date)
242
+ end
243
+
244
+ should "not persist changes to the live association" do
245
+ assert_equal [], @customer.orders.reload
246
+ end
247
+ end
248
+ end
249
+ end
250
+
251
+ context "and then the associated is destroyed" do
252
+ setup do
253
+ @order.destroy
254
+ end
255
+
256
+ context "when reified" do
257
+ setup { @customer_1 = @customer.versions.last.reify(has_many: true) }
258
+
259
+ should "see the associated as it was at the time" do
260
+ assert_equal [@order.order_date], @customer_1.orders.map(&:order_date)
261
+ end
262
+
263
+ should "not persist changes to the live association" do
264
+ assert_equal [], @customer.orders.reload
265
+ end
266
+ end
267
+ end
268
+
269
+ context "and then the associated is destroyed between model versions" do
270
+ setup do
271
+ @order.destroy
272
+ Timecop.travel 1.second.since
273
+ @customer.update_attributes name: "customer_2"
274
+ end
275
+
276
+ context "when reified" do
277
+ setup { @customer_1 = @customer.versions.last.reify(has_many: true) }
278
+
279
+ should "see the associated as it was at the time" do
280
+ assert_equal [], @customer_1.orders
281
+ end
282
+ end
283
+ end
284
+
285
+ context "and then another association is added" do
286
+ setup do
287
+ @customer.orders.create! order_date: "order_date_1"
288
+ end
289
+
290
+ context "when reified" do
291
+ setup { @customer_0 = @customer.versions.last.reify(has_many: true) }
292
+
293
+ should "see the associated as it was at the time" do
294
+ assert_equal ["order_date_0"], @customer_0.orders.map(&:order_date)
295
+ end
296
+
297
+ should "not persist changes to the live association" do
298
+ assert_equal %w(order_date_0 order_date_1),
299
+ @customer.orders.reload.map(&:order_date).sort
300
+ end
301
+ end
302
+
303
+ context "when reified with option mark_for_destruction" do
304
+ should "mark the newly associated for destruction" do
305
+ @customer_0 = @customer.versions.last.reify(
306
+ has_many: true,
307
+ mark_for_destruction: true
308
+ )
309
+ assert @customer_0.
310
+ orders.
311
+ detect { |o| o.order_date == "order_date_1" }.
312
+ marked_for_destruction?
313
+ end
314
+ end
315
+ end
316
+ end
317
+ end
318
+
319
+ context "has_many through associations" do
320
+ context "Books, Authors, and Authorships" do
321
+ setup { @book = Book.create title: "book_0" }
322
+
323
+ context "updated before the associated was created" do
324
+ setup do
325
+ @book.update_attributes! title: "book_1"
326
+ @book.authors.create! name: "author_0"
327
+ end
328
+
329
+ context "when reified" do
330
+ setup { @book_0 = @book.versions.last.reify(has_many: true) }
331
+
332
+ should "see the associated as it was at the time" do
333
+ assert_equal [], @book_0.authors
334
+ end
335
+
336
+ should "not persist changes to the live association" do
337
+ assert_equal ["author_0"], @book.authors.reload.map(&:name)
338
+ end
339
+ end
340
+
341
+ context "when reified with option mark_for_destruction" do
342
+ setup do
343
+ @book_0 = @book.versions.last.reify(
344
+ has_many: true,
345
+ mark_for_destruction: true
346
+ )
347
+ end
348
+
349
+ should "mark the associated for destruction" do
350
+ assert_equal [true], @book_0.authors.map(&:marked_for_destruction?)
351
+ end
352
+
353
+ should "mark the associated-through for destruction" do
354
+ assert_equal [true], @book_0.authorships.map(&:marked_for_destruction?)
355
+ end
356
+ end
357
+ end
358
+
359
+ context "updated before it is associated with an existing one" do
360
+ setup do
361
+ person_existing = Person.create(name: "person_existing")
362
+ Timecop.travel 1.second.since
363
+ @book.update_attributes! title: "book_1"
364
+ @book.authors << person_existing
365
+ end
366
+
367
+ context "when reified" do
368
+ setup do
369
+ @book_0 = @book.versions.last.reify(has_many: true)
370
+ end
371
+
372
+ should "see the associated as it was at the time" do
373
+ assert_equal [], @book_0.authors
374
+ end
375
+ end
376
+
377
+ context "when reified with option mark_for_destruction" do
378
+ setup do
379
+ @book_0 = @book.versions.last.reify(
380
+ has_many: true,
381
+ mark_for_destruction: true
382
+ )
383
+ end
384
+
385
+ should "not mark the associated for destruction" do
386
+ assert_equal [false], @book_0.authors.map(&:marked_for_destruction?)
387
+ end
388
+
389
+ should "mark the associated-through for destruction" do
390
+ assert_equal [true], @book_0.authorships.map(&:marked_for_destruction?)
391
+ end
392
+ end
393
+ end
394
+
395
+ context "where the association is created between model versions" do
396
+ setup do
397
+ @author = @book.authors.create! name: "author_0"
398
+ @person_existing = Person.create(name: "person_existing")
399
+ Timecop.travel 1.second.since
400
+ @book.update_attributes! title: "book_1"
401
+ end
402
+
403
+ context "when reified" do
404
+ setup { @book_0 = @book.versions.last.reify(has_many: true) }
405
+
406
+ should "see the associated as it was at the time" do
407
+ assert_equal ["author_0"], @book_0.authors.map(&:name)
408
+ end
409
+ end
410
+
411
+ context "and then the associated is updated between model versions" do
412
+ setup do
413
+ @author.update_attributes name: "author_1"
414
+ @author.update_attributes name: "author_2"
415
+ Timecop.travel 1.second.since
416
+ @book.update_attributes title: "book_2"
417
+ @author.update_attributes name: "author_3"
418
+ end
419
+
420
+ context "when reified" do
421
+ setup { @book_1 = @book.versions.last.reify(has_many: true) }
422
+
423
+ should "see the associated as it was at the time" do
424
+ assert_equal ["author_2"], @book_1.authors.map(&:name)
425
+ end
426
+
427
+ should "not persist changes to the live association" do
428
+ assert_equal ["author_3"], @book.authors.reload.map(&:name)
429
+ end
430
+ end
431
+
432
+ context "when reified opting out of has_many reification" do
433
+ setup { @book_1 = @book.versions.last.reify(has_many: false) }
434
+
435
+ should "see the associated as it is live" do
436
+ assert_equal ["author_3"], @book_1.authors.map(&:name)
437
+ end
438
+ end
439
+ end
440
+
441
+ context "and then the associated is destroyed" do
442
+ setup do
443
+ @author.destroy
444
+ end
445
+
446
+ context "when reified" do
447
+ setup { @book_1 = @book.versions.last.reify(has_many: true) }
448
+
449
+ should "see the associated as it was at the time" do
450
+ assert_equal [@author.name], @book_1.authors.map(&:name)
451
+ end
452
+
453
+ should "not persist changes to the live association" do
454
+ assert_equal [], @book.authors.reload
455
+ end
456
+ end
457
+ end
458
+
459
+ context "and then the associated is destroyed between model versions" do
460
+ setup do
461
+ @author.destroy
462
+ Timecop.travel 1.second.since
463
+ @book.update_attributes title: "book_2"
464
+ end
465
+
466
+ context "when reified" do
467
+ setup { @book_1 = @book.versions.last.reify(has_many: true) }
468
+
469
+ should "see the associated as it was at the time" do
470
+ assert_equal [], @book_1.authors
471
+ end
472
+ end
473
+ end
474
+
475
+ context "and then the associated is dissociated between model versions" do
476
+ setup do
477
+ @book.authors = []
478
+ Timecop.travel 1.second.since
479
+ @book.update_attributes title: "book_2"
480
+ end
481
+
482
+ context "when reified" do
483
+ setup { @book_1 = @book.versions.last.reify(has_many: true) }
484
+
485
+ should "see the associated as it was at the time" do
486
+ assert_equal [], @book_1.authors
487
+ end
488
+ end
489
+ end
490
+
491
+ context "and then another associated is created" do
492
+ setup do
493
+ @book.authors.create! name: "author_1"
494
+ end
495
+
496
+ context "when reified" do
497
+ setup { @book_0 = @book.versions.last.reify(has_many: true) }
498
+
499
+ should "only see the first associated" do
500
+ assert_equal ["author_0"], @book_0.authors.map(&:name)
501
+ end
502
+
503
+ should "not persist changes to the live association" do
504
+ assert_equal %w(author_0 author_1), @book.authors.reload.map(&:name)
505
+ end
506
+ end
507
+
508
+ context "when reified with option mark_for_destruction" do
509
+ setup do
510
+ @book_0 = @book.versions.last.reify(
511
+ has_many: true,
512
+ mark_for_destruction: true
513
+ )
514
+ end
515
+
516
+ should "mark the newly associated for destruction" do
517
+ assert @book_0.
518
+ authors.
519
+ detect { |a| a.name == "author_1" }.
520
+ marked_for_destruction?
521
+ end
522
+
523
+ should "mark the newly associated-through for destruction" do
524
+ assert @book_0.
525
+ authorships.
526
+ detect { |as| as.author.name == "author_1" }.
527
+ marked_for_destruction?
528
+ end
529
+ end
530
+ end
531
+
532
+ context "and then an existing one is associated" do
533
+ setup do
534
+ @book.authors << @person_existing
535
+ end
536
+
537
+ context "when reified" do
538
+ setup { @book_0 = @book.versions.last.reify(has_many: true) }
539
+
540
+ should "only see the first associated" do
541
+ assert_equal ["author_0"], @book_0.authors.map(&:name)
542
+ end
543
+
544
+ should "not persist changes to the live association" do
545
+ assert_equal %w(author_0 person_existing), @book.authors.reload.map(&:name).sort
546
+ end
547
+ end
548
+
549
+ context "when reified with option mark_for_destruction" do
550
+ setup do
551
+ @book_0 = @book.versions.last.reify(
552
+ has_many: true,
553
+ mark_for_destruction: true
554
+ )
555
+ end
556
+
557
+ should "not mark the newly associated for destruction" do
558
+ assert !@book_0.
559
+ authors.
560
+ detect { |a| a.name == "person_existing" }.
561
+ marked_for_destruction?
562
+ end
563
+
564
+ should "mark the newly associated-through for destruction" do
565
+ assert @book_0.
566
+ authorships.
567
+ detect { |as| as.author.name == "person_existing" }.
568
+ marked_for_destruction?
569
+ end
570
+ end
571
+ end
572
+ end
573
+
574
+ context "updated before the associated without paper_trail was created" do
575
+ setup do
576
+ @book.update_attributes! title: "book_1"
577
+ @book.editors.create! name: "editor_0"
578
+ end
579
+
580
+ context "when reified" do
581
+ setup { @book_0 = @book.versions.last.reify(has_many: true) }
582
+
583
+ should "see the live association" do
584
+ assert_equal ["editor_0"], @book_0.editors.map(&:name)
585
+ end
586
+ end
587
+ end
588
+ end
589
+
590
+ context "Chapters, Sections, Paragraphs, Quotations, and Citations" do
591
+ setup { @chapter = Chapter.create(name: CHAPTER_NAMES[0]) }
592
+
593
+ context "before any associations are created" do
594
+ setup do
595
+ @chapter.update_attributes(name: CHAPTER_NAMES[1])
596
+ end
597
+
598
+ should "not reify any associations" do
599
+ chapter_v1 = @chapter.versions[1].reify(has_many: true)
600
+ assert_equal CHAPTER_NAMES[0], chapter_v1.name
601
+ assert_equal [], chapter_v1.sections
602
+ assert_equal [], chapter_v1.paragraphs
603
+ end
604
+ end
605
+
606
+ context "after the first has_many through relationship is created" do
607
+ setup do
608
+ assert_equal 1, @chapter.versions.size
609
+ @chapter.update_attributes name: CHAPTER_NAMES[1]
610
+ assert_equal 2, @chapter.versions.size
611
+
612
+ Timecop.travel 1.second.since
613
+ @chapter.sections.create name: "section 1"
614
+ Timecop.travel 1.second.since
615
+ @chapter.sections.first.update_attributes name: "section 2"
616
+ Timecop.travel 1.second.since
617
+ @chapter.update_attributes name: CHAPTER_NAMES[2]
618
+ assert_equal 3, @chapter.versions.size
619
+
620
+ Timecop.travel 1.second.since
621
+ @chapter.sections.first.update_attributes name: "section 3"
622
+ end
623
+
624
+ context "version 1" do
625
+ should "have no sections" do
626
+ chapter_v1 = @chapter.versions[1].reify(has_many: true)
627
+ assert_equal [], chapter_v1.sections
628
+ end
629
+ end
630
+
631
+ context "version 2" do
632
+ should "have one section" do
633
+ chapter_v2 = @chapter.versions[2].reify(has_many: true)
634
+ assert_equal 1, chapter_v2.sections.size
635
+
636
+ # Shows the value of the section as it was before
637
+ # the chapter was updated.
638
+ assert_equal ["section 2"], chapter_v2.sections.map(&:name)
639
+
640
+ # Shows the value of the chapter as it was before
641
+ assert_equal CHAPTER_NAMES[1], chapter_v2.name
642
+ end
643
+ end
644
+
645
+ context "version 2, before the section was destroyed" do
646
+ setup do
647
+ @chapter.update_attributes name: CHAPTER_NAMES[2]
648
+ Timecop.travel 1.second.since
649
+ @chapter.sections.destroy_all
650
+ Timecop.travel 1.second.since
651
+ end
652
+
653
+ should "have the one section" do
654
+ chapter_v2 = @chapter.versions[2].reify(has_many: true)
655
+ assert_equal ["section 2"], chapter_v2.sections.map(&:name)
656
+ end
657
+ end
658
+
659
+ context "version 3, after the section was destroyed" do
660
+ setup do
661
+ @chapter.sections.destroy_all
662
+ Timecop.travel 1.second.since
663
+ @chapter.update_attributes name: CHAPTER_NAMES[3]
664
+ Timecop.travel 1.second.since
665
+ end
666
+
667
+ should "have no sections" do
668
+ chapter_v3 = @chapter.versions[3].reify(has_many: true)
669
+ assert_equal 0, chapter_v3.sections.size
670
+ end
671
+ end
672
+
673
+ context "after creating a paragraph" do
674
+ setup do
675
+ assert_equal 3, @chapter.versions.size
676
+ @section = @chapter.sections.first
677
+ Timecop.travel 1.second.since
678
+ @paragraph = @section.paragraphs.create name: "para1"
679
+ end
680
+
681
+ context "new chapter version" do
682
+ should "have one paragraph" do
683
+ initial_section_name = @section.name
684
+ initial_paragraph_name = @paragraph.name
685
+ Timecop.travel 1.second.since
686
+ @chapter.update_attributes name: CHAPTER_NAMES[4]
687
+ assert_equal 4, @chapter.versions.size
688
+ Timecop.travel 1.second.since
689
+ @paragraph.update_attributes name: "para3"
690
+ chapter_v3 = @chapter.versions[3].reify(has_many: true)
691
+ assert_equal [initial_section_name], chapter_v3.sections.map(&:name)
692
+ paragraphs = chapter_v3.sections.first.paragraphs
693
+ assert_equal 1, paragraphs.size
694
+ assert_equal [initial_paragraph_name], paragraphs.map(&:name)
695
+ end
696
+ end
697
+
698
+ context "the version before a section is destroyed" do
699
+ should "have the section and paragraph" do
700
+ Timecop.travel 1.second.since
701
+ @chapter.update_attributes(name: CHAPTER_NAMES[3])
702
+ assert_equal 4, @chapter.versions.size
703
+ Timecop.travel 1.second.since
704
+ @section.destroy
705
+ assert_equal 4, @chapter.versions.size
706
+ chapter_v3 = @chapter.versions[3].reify(has_many: true)
707
+ assert_equal CHAPTER_NAMES[2], chapter_v3.name
708
+ assert_equal [@section], chapter_v3.sections
709
+ assert_equal [@paragraph], chapter_v3.sections[0].paragraphs
710
+ assert_equal [@paragraph], chapter_v3.paragraphs
711
+ end
712
+ end
713
+
714
+ context "the version after a section is destroyed" do
715
+ should "not have any sections or paragraphs" do
716
+ @section.destroy
717
+ Timecop.travel 1.second.since
718
+ @chapter.update_attributes(name: CHAPTER_NAMES[5])
719
+ assert_equal 4, @chapter.versions.size
720
+ chapter_v3 = @chapter.versions[3].reify(has_many: true)
721
+ assert_equal 0, chapter_v3.sections.size
722
+ assert_equal 0, chapter_v3.paragraphs.size
723
+ end
724
+ end
725
+
726
+ context "the version before a paragraph is destroyed" do
727
+ should "have the one paragraph" do
728
+ initial_paragraph_name = @section.paragraphs.first.name
729
+ Timecop.travel 1.second.since
730
+ @chapter.update_attributes(name: CHAPTER_NAMES[5])
731
+ Timecop.travel 1.second.since
732
+ @paragraph.destroy
733
+ chapter_v3 = @chapter.versions[3].reify(has_many: true)
734
+ paragraphs = chapter_v3.sections.first.paragraphs
735
+ assert_equal 1, paragraphs.size
736
+ assert_equal initial_paragraph_name, paragraphs.first.name
737
+ end
738
+ end
739
+
740
+ context "the version after a paragraph is destroyed" do
741
+ should "have no paragraphs" do
742
+ @paragraph.destroy
743
+ Timecop.travel 1.second.since
744
+ @chapter.update_attributes(name: CHAPTER_NAMES[5])
745
+ chapter_v3 = @chapter.versions[3].reify(has_many: true)
746
+ assert_equal 0, chapter_v3.paragraphs.size
747
+ assert_equal [], chapter_v3.sections.first.paragraphs
748
+ end
749
+ end
750
+ end
751
+ end
752
+
753
+ context "a chapter with one paragraph and one citation" do
754
+ should "reify paragraphs and citations" do
755
+ chapter = Chapter.create(name: CHAPTER_NAMES[0])
756
+ section = Section.create(name: "Section One", chapter: chapter)
757
+ paragraph = Paragraph.create(name: "Paragraph One", section: section)
758
+ quotation = Quotation.create(chapter: chapter)
759
+ citation = Citation.create(quotation: quotation)
760
+ Timecop.travel 1.second.since
761
+ chapter.update_attributes(name: CHAPTER_NAMES[1])
762
+ assert_equal 2, chapter.versions.count
763
+ paragraph.destroy
764
+ citation.destroy
765
+ reified = chapter.versions[1].reify(has_many: true)
766
+ assert_equal [paragraph], reified.sections.first.paragraphs
767
+ assert_equal [citation], reified.quotations.first.citations
768
+ end
769
+ end
770
+ end
771
+ end
772
+
773
+ context "belongs_to associations" do
774
+ context "Wotsit and Widget" do
775
+ setup { @widget = Widget.create(name: "widget_0") }
776
+
777
+ context "where the association is created between model versions" do
778
+ setup do
779
+ @wotsit = Wotsit.create(name: "wotsit_0")
780
+ Timecop.travel 1.second.since
781
+ @wotsit.update_attributes widget_id: @widget.id, name: "wotsit_1"
782
+ end
783
+
784
+ context "when reified" do
785
+ setup { @wotsit_0 = @wotsit.versions.last.reify(belongs_to: true) }
786
+
787
+ should "see the associated as it was at the time" do
788
+ assert_equal nil, @wotsit_0.widget
789
+ end
790
+
791
+ should "not persist changes to the live association" do
792
+ assert_equal @widget, @wotsit.reload.widget
793
+ end
794
+ end
795
+
796
+ context "and then the associated is updated between model versions" do
797
+ setup do
798
+ @widget.update_attributes name: "widget_1"
799
+ @widget.update_attributes name: "widget_2"
800
+ Timecop.travel 1.second.since
801
+ @wotsit.update_attributes name: "wotsit_2"
802
+ @widget.update_attributes name: "widget_3"
803
+ end
804
+
805
+ context "when reified" do
806
+ setup { @wotsit_1 = @wotsit.versions.last.reify(belongs_to: true) }
807
+
808
+ should "see the associated as it was at the time" do
809
+ assert_equal "widget_2", @wotsit_1.widget.name
810
+ end
811
+
812
+ should "not persist changes to the live association" do
813
+ assert_equal "widget_3", @wotsit.reload.widget.name
814
+ end
815
+ end
816
+
817
+ context "when reified opting out of belongs_to reification" do
818
+ setup { @wotsit_1 = @wotsit.versions.last.reify(belongs_to: false) }
819
+
820
+ should "see the associated as it is live" do
821
+ assert_equal "widget_3", @wotsit_1.widget.name
822
+ end
823
+ end
824
+ end
825
+
826
+ context "and then the associated is destroyed" do
827
+ setup do
828
+ @wotsit.update_attributes name: "wotsit_2"
829
+ @widget.destroy
830
+ end
831
+
832
+ context "when reified" do
833
+ setup { @wotsit_2 = @wotsit.versions.last.reify(belongs_to: true) }
834
+
835
+ should "see the associated as it was at the time" do
836
+ assert_equal @widget, @wotsit_2.widget
837
+ end
838
+
839
+ should "not persist changes to the live association" do
840
+ assert_nil @wotsit.reload.widget
841
+ end
842
+ end
843
+
844
+ context "and then the model is updated" do
845
+ setup do
846
+ Timecop.travel 1.second.since
847
+ @wotsit.update_attributes name: "wotsit_3"
848
+ end
849
+
850
+ context "when reified" do
851
+ setup { @wotsit_2 = @wotsit.versions.last.reify(belongs_to: true) }
852
+
853
+ should "see the associated as it was the time" do
854
+ assert_nil @wotsit_2.widget
855
+ end
856
+ end
857
+ end
858
+ end
859
+ end
860
+
861
+ context "where the association is changed between model versions" do
862
+ setup do
863
+ @wotsit = @widget.create_wotsit(name: "wotsit_0")
864
+ Timecop.travel 1.second.since
865
+ @new_widget = Widget.create(name: "new_widget")
866
+ @wotsit.update_attributes(widget_id: @new_widget.id, name: "wotsit_1")
867
+ end
868
+
869
+ context "when reified" do
870
+ setup { @wotsit_0 = @wotsit.versions.last.reify(belongs_to: true) }
871
+
872
+ should "see the association as it was at the time" do
873
+ assert_equal "widget_0", @wotsit_0.widget.name
874
+ end
875
+
876
+ should "not persist changes to the live association" do
877
+ assert_equal @new_widget, @wotsit.reload.widget
878
+ end
879
+ end
880
+
881
+ context "when reified with option mark_for_destruction" do
882
+ setup do
883
+ @wotsit_0 = @wotsit.versions.last.
884
+ reify(belongs_to: true, mark_for_destruction: true)
885
+ end
886
+
887
+ should "should not mark the new associated for destruction" do
888
+ assert_equal false, @new_widget.marked_for_destruction?
889
+ end
890
+ end
891
+ end
892
+ end
893
+ end
894
+
895
+ context "has_and_belongs_to_many associations" do
896
+ context "foo and bar" do
897
+ setup do
898
+ @foo = FooHabtm.create(name: "foo")
899
+ Timecop.travel 1.second.since
900
+ end
901
+
902
+ context "where the association is created between model versions" do
903
+ setup do
904
+ @foo.update_attributes(name: "foo1", bar_habtms: [BarHabtm.create(name: "bar")])
905
+ end
906
+
907
+ context "when reified" do
908
+ setup { @reified = @foo.versions.last.reify(has_and_belongs_to_many: true) }
909
+
910
+ should "see the associated as it was at the time" do
911
+ assert_equal 0, @reified.bar_habtms.length
912
+ end
913
+
914
+ should "not persist changes to the live association" do
915
+ assert_not_equal @reified.bar_habtms, @foo.reload.bar_habtms
916
+ end
917
+ end
918
+ end
919
+
920
+ context "where the association is changed between model versions" do
921
+ setup do
922
+ @foo.update_attributes(name: "foo2", bar_habtms: [BarHabtm.create(name: "bar2")])
923
+ Timecop.travel 1.second.since
924
+ @foo.update_attributes(name: "foo3", bar_habtms: [BarHabtm.create(name: "bar3")])
925
+ end
926
+
927
+ context "when reified" do
928
+ setup { @reified = @foo.versions.last.reify(has_and_belongs_to_many: true) }
929
+
930
+ should "see the association as it was at the time" do
931
+ assert_equal "bar2", @reified.bar_habtms.first.name
932
+ end
933
+
934
+ should "not persist changes to the live association" do
935
+ assert_not_equal @reified.bar_habtms.first, @foo.reload.bar_habtms.first
936
+ end
937
+ end
938
+
939
+ context "when reified with has_and_belongs_to_many: false" do
940
+ setup { @reified = @foo.versions.last.reify }
941
+
942
+ should "see the association as it is now" do
943
+ assert_equal "bar3", @reified.bar_habtms.first.name
944
+ end
945
+ end
946
+ end
947
+
948
+ context "where the association is destroyed between model versions" do
949
+ setup do
950
+ @foo.update_attributes(name: "foo2", bar_habtms: [BarHabtm.create(name: "bar2")])
951
+ Timecop.travel 1.second.since
952
+ @foo.update_attributes(name: "foo3", bar_habtms: [])
953
+ end
954
+
955
+ context "when reified" do
956
+ setup { @reified = @foo.versions.last.reify(has_and_belongs_to_many: true) }
957
+
958
+ should "see the association as it was at the time" do
959
+ assert_equal "bar2", @reified.bar_habtms.first.name
960
+ end
961
+
962
+ should "not persist changes to the live association" do
963
+ assert_not_equal @reified.bar_habtms.first, @foo.reload.bar_habtms.first
964
+ end
965
+ end
966
+ end
967
+
968
+ context "where the unassociated model changes" do
969
+ setup do
970
+ @bar = BarHabtm.create(name: "bar2")
971
+ @foo.update_attributes(name: "foo2", bar_habtms: [@bar])
972
+ Timecop.travel 1.second.since
973
+ @foo.update_attributes(name: "foo3", bar_habtms: [BarHabtm.create(name: "bar4")])
974
+ Timecop.travel 1.second.since
975
+ @bar.update_attributes(name: "bar3")
976
+ end
977
+
978
+ context "when reified" do
979
+ setup { @reified = @foo.versions.last.reify(has_and_belongs_to_many: true) }
980
+
981
+ should "see the association as it was at the time" do
982
+ assert_equal "bar2", @reified.bar_habtms.first.name
983
+ end
984
+
985
+ should "not persist changes to the live association" do
986
+ assert_not_equal @reified.bar_habtms.first, @foo.reload.bar_habtms.first
987
+ end
988
+ end
989
+ end
990
+ end
991
+
992
+ context "updated via nested attributes" do
993
+ setup do
994
+ @foo = FooHabtm.create(
995
+ name: "foo",
996
+ bar_habtms_attributes: [{ name: "bar" }]
997
+ )
998
+ Timecop.travel 1.second.since
999
+ @foo.update_attributes(
1000
+ name: "foo2",
1001
+ bar_habtms_attributes: [{ id: @foo.bar_habtms.first.id, name: "bar2" }]
1002
+ )
1003
+
1004
+ @reified = @foo.versions.last.reify(has_and_belongs_to_many: true)
1005
+ end
1006
+
1007
+ should "see the associated object as it was at the time" do
1008
+ assert_equal "bar", @reified.bar_habtms.first.name
1009
+ end
1010
+
1011
+ should "not persist changes to the live object" do
1012
+ assert_not_equal @reified.bar_habtms.first.name, @foo.reload.bar_habtms.first.name
1013
+ end
1014
+ end
1015
+ end
1016
+ end