paper_trail 7.0.2 → 7.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CONTRIBUTING.md +1 -1
  3. data/.rubocop_todo.yml +10 -0
  4. data/.travis.yml +4 -4
  5. data/Appraisals +3 -5
  6. data/CHANGELOG.md +16 -1
  7. data/README.md +70 -119
  8. data/Rakefile +6 -1
  9. data/doc/bug_report_template.rb +4 -2
  10. data/doc/warning_about_not_setting_whodunnit.md +6 -5
  11. data/gemfiles/ar_4.0.gemfile +1 -1
  12. data/gemfiles/ar_4.2.gemfile +1 -1
  13. data/gemfiles/ar_5.0.gemfile +2 -3
  14. data/gemfiles/ar_5.1.gemfile +8 -0
  15. data/gemfiles/ar_master.gemfile +2 -2
  16. data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb.erb +1 -1
  17. data/lib/generators/paper_trail/templates/add_transaction_id_column_to_versions.rb.erb +1 -1
  18. data/lib/generators/paper_trail/templates/create_version_associations.rb.erb +1 -1
  19. data/lib/paper_trail/model_config.rb +4 -4
  20. data/lib/paper_trail/version_concern.rb +4 -4
  21. data/lib/paper_trail/version_number.rb +1 -1
  22. data/paper_trail.gemspec +5 -5
  23. data/spec/controllers/articles_controller_spec.rb +1 -1
  24. data/spec/generators/install_generator_spec.rb +2 -2
  25. data/spec/models/animal_spec.rb +5 -5
  26. data/spec/models/boolit_spec.rb +2 -2
  27. data/spec/models/callback_modifier_spec.rb +2 -2
  28. data/spec/models/car_spec.rb +2 -2
  29. data/spec/models/custom_primary_key_record_spec.rb +2 -2
  30. data/spec/models/document_spec.rb +2 -2
  31. data/spec/models/gadget_spec.rb +2 -2
  32. data/spec/models/joined_version_spec.rb +1 -1
  33. data/spec/models/json_version_spec.rb +4 -4
  34. data/spec/models/kitchen/banana_spec.rb +2 -2
  35. data/spec/models/not_on_update_spec.rb +2 -2
  36. data/spec/models/post_with_status_spec.rb +4 -4
  37. data/spec/models/skipper_spec.rb +1 -1
  38. data/spec/models/thing_spec.rb +2 -2
  39. data/spec/models/vehicle_spec.rb +2 -2
  40. data/spec/models/version_spec.rb +26 -5
  41. data/spec/models/widget_spec.rb +10 -2
  42. data/spec/modules/paper_trail_spec.rb +2 -2
  43. data/spec/modules/version_concern_spec.rb +2 -2
  44. data/spec/modules/version_number_spec.rb +1 -1
  45. data/spec/paper_trail/associations_spec.rb +965 -0
  46. data/spec/paper_trail/cleaner_spec.rb +2 -2
  47. data/spec/paper_trail/config_spec.rb +2 -2
  48. data/spec/paper_trail/model_spec.rb +1421 -0
  49. data/spec/paper_trail/serializer_spec.rb +85 -0
  50. data/spec/paper_trail/serializers/custom_yaml_serializer_spec.rb +1 -1
  51. data/spec/paper_trail/serializers/json_spec.rb +2 -2
  52. data/spec/paper_trail/serializers/yaml_spec.rb +42 -0
  53. data/spec/paper_trail/version_limit_spec.rb +2 -2
  54. data/spec/paper_trail/version_spec.rb +96 -0
  55. data/spec/paper_trail_spec.rb +1 -1
  56. data/spec/requests/articles_spec.rb +2 -2
  57. data/spec/spec_helper.rb +47 -79
  58. data/{test → spec/support}/custom_json_serializer.rb +0 -0
  59. data/test/dummy/app/models/document.rb +1 -1
  60. data/test/dummy/app/models/not_on_update.rb +1 -1
  61. data/test/dummy/app/models/widget.rb +1 -1
  62. data/test/dummy/config/routes.rb +1 -1
  63. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +18 -9
  64. data/test/dummy/db/schema.rb +64 -64
  65. data/test/test_helper.rb +1 -33
  66. data/test/unit/serializers/mixin_json_test.rb +1 -1
  67. metadata +27 -32
  68. data/spec/models/truck_spec.rb +0 -5
  69. data/spec/rails_helper.rb +0 -34
  70. data/test/time_travel_helper.rb +0 -1
  71. data/test/unit/associations_test.rb +0 -1032
  72. data/test/unit/model_test.rb +0 -1416
  73. data/test/unit/serializer_test.rb +0 -107
  74. data/test/unit/serializers/yaml_test.rb +0 -50
  75. data/test/unit/version_test.rb +0 -112
@@ -1,6 +1,6 @@
1
- require "rails_helper"
1
+ require "spec_helper"
2
2
 
3
- describe Thing, type: :model do
3
+ RSpec.describe Thing, type: :model do
4
4
  it { is_expected.to be_versioned }
5
5
 
6
6
  describe "does not store object_changes", versioning: true do
@@ -1,5 +1,5 @@
1
- require "rails_helper"
1
+ require "spec_helper"
2
2
 
3
- describe Vehicle, type: :model do
3
+ RSpec.describe Vehicle, type: :model do
4
4
  it { is_expected.not_to be_versioned }
5
5
  end
@@ -1,7 +1,7 @@
1
- require "rails_helper"
1
+ require "spec_helper"
2
2
 
3
3
  module PaperTrail
4
- describe Version, type: :model do
4
+ ::RSpec.describe Version, type: :model do
5
5
  describe "object_changes column", versioning: true do
6
6
  let(:widget) { Widget.create!(name: "Dashboard") }
7
7
  let(:value) { widget.versions.last.object_changes }
@@ -93,6 +93,10 @@ module PaperTrail
93
93
  end
94
94
 
95
95
  describe "Methods" do
96
+ # TODO: Changing the data type of these database columns in the middle
97
+ # of the test suite adds a fair amount of complication. Is there a better
98
+ # way? We already have a `json_versions` table in our tests, maybe we
99
+ # could use that and add a `jsonb_versions` table?
96
100
  column_overrides = [false]
97
101
  if ENV["DB"] == "postgres" && ::ActiveRecord::VERSION::MAJOR >= 4
98
102
  column_overrides << "json"
@@ -104,8 +108,12 @@ module PaperTrail
104
108
  context "with a #{override || 'text'} column" do
105
109
  before do
106
110
  if override
107
- ActiveRecord::Base.connection.execute("SAVEPOINT pgtest;")
108
- %w(object object_changes).each do |column|
111
+ # In rails < 5, we use truncation, ie. there is no transaction
112
+ # around the tests, so we can't use a savepoint.
113
+ if active_record_gem_version >= ::Gem::Version.new("5")
114
+ ActiveRecord::Base.connection.execute("SAVEPOINT pgtest;")
115
+ end
116
+ %w[object object_changes].each do |column|
109
117
  ActiveRecord::Base.connection.execute(
110
118
  "ALTER TABLE versions DROP COLUMN #{column};"
111
119
  )
@@ -119,7 +127,20 @@ module PaperTrail
119
127
 
120
128
  after do
121
129
  if override
122
- ActiveRecord::Base.connection.execute("ROLLBACK TO SAVEPOINT pgtest;")
130
+ # In rails < 5, we use truncation, ie. there is no transaction
131
+ # around the tests, so we can't use a savepoint.
132
+ if active_record_gem_version >= ::Gem::Version.new("5")
133
+ ActiveRecord::Base.connection.execute("ROLLBACK TO SAVEPOINT pgtest;")
134
+ else
135
+ %w[object object_changes].each do |column|
136
+ ActiveRecord::Base.connection.execute(
137
+ "ALTER TABLE versions DROP COLUMN #{column};"
138
+ )
139
+ ActiveRecord::Base.connection.execute(
140
+ "ALTER TABLE versions ADD COLUMN #{column} text;"
141
+ )
142
+ end
143
+ end
123
144
  PaperTrail::Version.reset_column_information
124
145
  end
125
146
  end
@@ -1,6 +1,6 @@
1
- require "rails_helper"
1
+ require "spec_helper"
2
2
 
3
- describe Widget, type: :model do
3
+ RSpec.describe Widget, type: :model do
4
4
  describe "`be_versioned` matcher" do
5
5
  it { is_expected.to be_versioned }
6
6
  end
@@ -317,6 +317,10 @@ describe Widget, type: :model do
317
317
  Widget.paper_trail.disable
318
318
  expect(Widget.paper_trail.enabled?).to eq(false)
319
319
  end
320
+
321
+ after do
322
+ Widget.paper_trail.enable
323
+ end
320
324
  end
321
325
 
322
326
  describe ".enable" do
@@ -326,5 +330,9 @@ describe Widget, type: :model do
326
330
  Widget.paper_trail.enable
327
331
  expect(Widget.paper_trail.enabled?).to eq(true)
328
332
  end
333
+
334
+ after do
335
+ Widget.paper_trail.enable
336
+ end
329
337
  end
330
338
  end
@@ -1,6 +1,6 @@
1
- require "rails_helper"
1
+ require "spec_helper"
2
2
 
3
- describe PaperTrail, type: :module, versioning: true do
3
+ RSpec.describe PaperTrail, type: :module, versioning: true do
4
4
  describe "#config" do
5
5
  it { is_expected.to respond_to(:config) }
6
6
 
@@ -1,6 +1,6 @@
1
- require "rails_helper"
1
+ require "spec_helper"
2
2
 
3
- describe PaperTrail::VersionConcern do
3
+ RSpec.describe PaperTrail::VersionConcern do
4
4
  before(:all) { require "support/alt_db_init" }
5
5
 
6
6
  it "allows included class to have different connections" do
@@ -1,7 +1,7 @@
1
1
  require "spec_helper"
2
2
 
3
3
  module PaperTrail
4
- RSpec.describe VERSION do
4
+ ::RSpec.describe VERSION do
5
5
  describe "STRING" do
6
6
  it "joins the numbers into a period separated string" do
7
7
  expect(described_class::STRING).to eq(
@@ -0,0 +1,965 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe(::PaperTrail, versioning: true) do
4
+ CHAPTER_NAMES = [
5
+ "Down the Rabbit-Hole",
6
+ "The Pool of Tears",
7
+ "A Caucus-Race and a Long Tale",
8
+ "The Rabbit Sends in a Little Bill",
9
+ "Advice from a Caterpillar",
10
+ "Pig and Pepper",
11
+ "A Mad Tea-Party",
12
+ "The Queen's Croquet-Ground",
13
+ "The Mock Turtle's Story",
14
+ "The Lobster Quadrille",
15
+ "Who Stole the Tarts?",
16
+ "Alice's Evidence"
17
+ ].freeze
18
+
19
+ after do
20
+ Timecop.return
21
+ end
22
+
23
+ context "a has_one association" do
24
+ before { @widget = Widget.create(name: "widget_0") }
25
+
26
+ context "before the associated was created" do
27
+ before do
28
+ @widget.update_attributes(name: "widget_1")
29
+ @wotsit = @widget.create_wotsit(name: "wotsit_0")
30
+ end
31
+
32
+ context "when reified" do
33
+ before { @widget0 = @widget.versions.last.reify(has_one: true) }
34
+
35
+ it "see the associated as it was at the time" do
36
+ expect(@widget0.wotsit).to be_nil
37
+ end
38
+
39
+ it "not persist changes to the live association" do
40
+ expect(@widget.reload.wotsit).to(eq(@wotsit))
41
+ end
42
+ end
43
+ end
44
+
45
+ context "where the association is created between model versions" do
46
+ before do
47
+ @wotsit = @widget.create_wotsit(name: "wotsit_0")
48
+ Timecop.travel(1.second.since)
49
+ @widget.update_attributes(name: "widget_1")
50
+ end
51
+
52
+ context "when reified" do
53
+ before { @widget0 = @widget.versions.last.reify(has_one: true) }
54
+
55
+ it "see the associated as it was at the time" do
56
+ expect(@widget0.wotsit.name).to(eq("wotsit_0"))
57
+ end
58
+
59
+ it "not persist changes to the live association" do
60
+ expect(@widget.reload.wotsit).to(eq(@wotsit))
61
+ end
62
+ end
63
+
64
+ context "and then the associated is updated between model versions" do
65
+ before do
66
+ @wotsit.update_attributes(name: "wotsit_1")
67
+ @wotsit.update_attributes(name: "wotsit_2")
68
+ Timecop.travel(1.second.since)
69
+ @widget.update_attributes(name: "widget_2")
70
+ @wotsit.update_attributes(name: "wotsit_3")
71
+ end
72
+
73
+ context "when reified" do
74
+ before { @widget1 = @widget.versions.last.reify(has_one: true) }
75
+
76
+ it "see the associated as it was at the time" do
77
+ expect(@widget1.wotsit.name).to(eq("wotsit_2"))
78
+ end
79
+
80
+ it "not persist changes to the live association" do
81
+ expect(@widget.reload.wotsit.name).to(eq("wotsit_3"))
82
+ end
83
+ end
84
+
85
+ context "when reified opting out of has_one reification" do
86
+ before { @widget1 = @widget.versions.last.reify(has_one: false) }
87
+
88
+ it "see the associated as it is live" do
89
+ expect(@widget1.wotsit.name).to(eq("wotsit_3"))
90
+ end
91
+ end
92
+ end
93
+
94
+ context "and then the associated is destroyed" do
95
+ before { @wotsit.destroy }
96
+
97
+ context "when reify" do
98
+ before { @widget1 = @widget.versions.last.reify(has_one: true) }
99
+
100
+ it "see the associated as it was at the time" do
101
+ expect(@widget1.wotsit).to(eq(@wotsit))
102
+ end
103
+
104
+ it "not persist changes to the live association" do
105
+ expect(@widget.reload.wotsit).to be_nil
106
+ end
107
+ end
108
+
109
+ context "and then the model is updated" do
110
+ before do
111
+ Timecop.travel(1.second.since)
112
+ @widget.update_attributes(name: "widget_3")
113
+ end
114
+
115
+ context "when reified" do
116
+ before { @widget2 = @widget.versions.last.reify(has_one: true) }
117
+
118
+ it "see the associated as it was at the time" do
119
+ expect(@widget2.wotsit).to be_nil
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ context "a has_many association" do
128
+ before { @customer = Customer.create(name: "customer_0") }
129
+
130
+ context "updated before the associated was created" do
131
+ before do
132
+ @customer.update_attributes!(name: "customer_1")
133
+ @customer.orders.create!(order_date: Date.today)
134
+ end
135
+
136
+ context "when reified" do
137
+ before { @customer0 = @customer.versions.last.reify(has_many: true) }
138
+
139
+ it "see the associated as it was at the time" do
140
+ expect(@customer0.orders).to(eq([]))
141
+ end
142
+
143
+ it "not persist changes to the live association" do
144
+ expect(@customer.orders.reload).not_to(eq([]))
145
+ end
146
+ end
147
+
148
+ context "when reified with option mark_for_destruction" do
149
+ it "mark the associated for destruction" do
150
+ @customer0 = @customer.versions.last.reify(has_many: true, mark_for_destruction: true)
151
+ expect(@customer0.orders.map(&:marked_for_destruction?)).to(eq([true]))
152
+ end
153
+ end
154
+ end
155
+
156
+ context "where the association is created between model versions" do
157
+ before do
158
+ @order = @customer.orders.create!(order_date: "order_date_0")
159
+ Timecop.travel(1.second.since)
160
+ @customer.update_attributes(name: "customer_1")
161
+ end
162
+
163
+ context "when reified" do
164
+ before { @customer0 = @customer.versions.last.reify(has_many: true) }
165
+
166
+ it "see the associated as it was at the time" do
167
+ expect(@customer0.orders.map(&:order_date)).to(eq(["order_date_0"]))
168
+ end
169
+ end
170
+
171
+ context "and then a nested has_many association is created" do
172
+ before { @order.line_items.create!(product: "product_0") }
173
+
174
+ context "when reified" do
175
+ before { @customer0 = @customer.versions.last.reify(has_many: true) }
176
+
177
+ it "see the live version of the nested association" do
178
+ expect(@customer0.orders.first.line_items.map(&:product)).to(eq(["product_0"]))
179
+ end
180
+ end
181
+ end
182
+
183
+ context "and then the associated is updated between model versions" do
184
+ before do
185
+ @order.update_attributes(order_date: "order_date_1")
186
+ @order.update_attributes(order_date: "order_date_2")
187
+ Timecop.travel(1.second.since)
188
+ @customer.update_attributes(name: "customer_2")
189
+ @order.update_attributes(order_date: "order_date_3")
190
+ end
191
+
192
+ context "when reified" do
193
+ before { @customer1 = @customer.versions.last.reify(has_many: true) }
194
+
195
+ it "see the associated as it was at the time" do
196
+ expect(@customer1.orders.map(&:order_date)).to(eq(["order_date_2"]))
197
+ end
198
+
199
+ it "not persist changes to the live association" do
200
+ expect(@customer.orders.reload.map(&:order_date)).to(eq(["order_date_3"]))
201
+ end
202
+ end
203
+
204
+ context "when reified opting out of has_many reification" do
205
+ before { @customer1 = @customer.versions.last.reify(has_many: false) }
206
+
207
+ it "see the associated as it is live" do
208
+ expect(@customer1.orders.map(&:order_date)).to(eq(["order_date_3"]))
209
+ end
210
+ end
211
+
212
+ context "and then the associated is destroyed" do
213
+ before { @order.destroy }
214
+
215
+ context "when reified" do
216
+ before { @customer1 = @customer.versions.last.reify(has_many: true) }
217
+
218
+ it "see the associated as it was at the time" do
219
+ expect(@customer1.orders.map(&:order_date)).to(eq(["order_date_2"]))
220
+ end
221
+
222
+ it "not persist changes to the live association" do
223
+ expect(@customer.orders.reload).to(eq([]))
224
+ end
225
+ end
226
+ end
227
+ end
228
+
229
+ context "and then the associated is destroyed" do
230
+ before { @order.destroy }
231
+
232
+ context "when reified" do
233
+ before { @customer1 = @customer.versions.last.reify(has_many: true) }
234
+
235
+ it "see the associated as it was at the time" do
236
+ expect(@customer1.orders.map(&:order_date)).to(eq([@order.order_date]))
237
+ end
238
+
239
+ it "not persist changes to the live association" do
240
+ expect(@customer.orders.reload).to(eq([]))
241
+ end
242
+ end
243
+ end
244
+
245
+ context "and then the associated is destroyed between model versions" do
246
+ before do
247
+ @order.destroy
248
+ Timecop.travel(1.second.since)
249
+ @customer.update_attributes(name: "customer_2")
250
+ end
251
+
252
+ context "when reified" do
253
+ before { @customer1 = @customer.versions.last.reify(has_many: true) }
254
+
255
+ it "see the associated as it was at the time" do
256
+ expect(@customer1.orders).to(eq([]))
257
+ end
258
+ end
259
+ end
260
+
261
+ context "and then another association is added" do
262
+ before { @customer.orders.create!(order_date: "order_date_1") }
263
+
264
+ context "when reified" do
265
+ before { @customer0 = @customer.versions.last.reify(has_many: true) }
266
+
267
+ it "see the associated as it was at the time" do
268
+ expect(@customer0.orders.map(&:order_date)).to(eq(["order_date_0"]))
269
+ end
270
+
271
+ it "not persist changes to the live association" do
272
+ expect(
273
+ @customer.orders.reload.map(&:order_date)
274
+ ).to match_array(%w[order_date_0 order_date_1])
275
+ end
276
+ end
277
+
278
+ context "when reified with option mark_for_destruction" do
279
+ it "mark the newly associated for destruction" do
280
+ @customer0 = @customer.versions.last.reify(has_many: true, mark_for_destruction: true)
281
+ order = @customer0.orders.detect { |o| o.order_date == "order_date_1" }
282
+ expect(order).to be_marked_for_destruction
283
+ end
284
+ end
285
+ end
286
+ end
287
+ end
288
+
289
+ context "has_many through associations" do
290
+ context "Books, Authors, and Authorships" do
291
+ before { @book = Book.create(title: "book_0") }
292
+
293
+ context "updated before the associated was created" do
294
+ before do
295
+ @book.update_attributes!(title: "book_1")
296
+ @book.authors.create!(name: "author_0")
297
+ end
298
+
299
+ context "when reified" do
300
+ before { @book0 = @book.versions.last.reify(has_many: true) }
301
+
302
+ it "see the associated as it was at the time" do
303
+ expect(@book0.authors).to(eq([]))
304
+ end
305
+
306
+ it "not persist changes to the live association" do
307
+ expect(@book.authors.reload.map(&:name)).to(eq(["author_0"]))
308
+ end
309
+ end
310
+
311
+ context "when reified with option mark_for_destruction" do
312
+ before do
313
+ @book0 = @book.versions.last.reify(has_many: true, mark_for_destruction: true)
314
+ end
315
+
316
+ it "mark the associated for destruction" do
317
+ expect(@book0.authors.map(&:marked_for_destruction?)).to(eq([true]))
318
+ end
319
+
320
+ it "mark the associated-through for destruction" do
321
+ expect(@book0.authorships.map(&:marked_for_destruction?)).to(eq([true]))
322
+ end
323
+ end
324
+ end
325
+
326
+ context "updated before it is associated with an existing one" do
327
+ before do
328
+ person_existing = Person.create(name: "person_existing")
329
+ Timecop.travel(1.second.since)
330
+ @book.update_attributes!(title: "book_1")
331
+ (@book.authors << person_existing)
332
+ end
333
+
334
+ context "when reified" do
335
+ before { @book0 = @book.versions.last.reify(has_many: true) }
336
+
337
+ it "see the associated as it was at the time" do
338
+ expect(@book0.authors).to(eq([]))
339
+ end
340
+ end
341
+
342
+ context "when reified with option mark_for_destruction" do
343
+ before do
344
+ @book0 = @book.versions.last.reify(has_many: true, mark_for_destruction: true)
345
+ end
346
+
347
+ it "not mark the associated for destruction" do
348
+ expect(@book0.authors.map(&:marked_for_destruction?)).to(eq([false]))
349
+ end
350
+
351
+ it "mark the associated-through for destruction" do
352
+ expect(@book0.authorships.map(&:marked_for_destruction?)).to(eq([true]))
353
+ end
354
+ end
355
+ end
356
+
357
+ context "where the association is created between model versions" do
358
+ before do
359
+ @author = @book.authors.create!(name: "author_0")
360
+ @person_existing = Person.create(name: "person_existing")
361
+ Timecop.travel(1.second.since)
362
+ @book.update_attributes!(title: "book_1")
363
+ end
364
+
365
+ context "when reified" do
366
+ before { @book0 = @book.versions.last.reify(has_many: true) }
367
+
368
+ it "see the associated as it was at the time" do
369
+ expect(@book0.authors.map(&:name)).to(eq(["author_0"]))
370
+ end
371
+ end
372
+
373
+ context "and then the associated is updated between model versions" do
374
+ before do
375
+ @author.update_attributes(name: "author_1")
376
+ @author.update_attributes(name: "author_2")
377
+ Timecop.travel(1.second.since)
378
+ @book.update_attributes(title: "book_2")
379
+ @author.update_attributes(name: "author_3")
380
+ end
381
+
382
+ context "when reified" do
383
+ before { @book1 = @book.versions.last.reify(has_many: true) }
384
+
385
+ it "see the associated as it was at the time" do
386
+ expect(@book1.authors.map(&:name)).to(eq(["author_2"]))
387
+ end
388
+
389
+ it "not persist changes to the live association" do
390
+ expect(@book.authors.reload.map(&:name)).to(eq(["author_3"]))
391
+ end
392
+ end
393
+
394
+ context "when reified opting out of has_many reification" do
395
+ before { @book1 = @book.versions.last.reify(has_many: false) }
396
+
397
+ it "see the associated as it is live" do
398
+ expect(@book1.authors.map(&:name)).to(eq(["author_3"]))
399
+ end
400
+ end
401
+ end
402
+
403
+ context "and then the associated is destroyed" do
404
+ before { @author.destroy }
405
+
406
+ context "when reified" do
407
+ before { @book1 = @book.versions.last.reify(has_many: true) }
408
+
409
+ it "see the associated as it was at the time" do
410
+ expect(@book1.authors.map(&:name)).to(eq([@author.name]))
411
+ end
412
+
413
+ it "not persist changes to the live association" do
414
+ expect(@book.authors.reload).to(eq([]))
415
+ end
416
+ end
417
+ end
418
+
419
+ context "and then the associated is destroyed between model versions" do
420
+ before do
421
+ @author.destroy
422
+ Timecop.travel(1.second.since)
423
+ @book.update_attributes(title: "book_2")
424
+ end
425
+
426
+ context "when reified" do
427
+ before { @book1 = @book.versions.last.reify(has_many: true) }
428
+
429
+ it "see the associated as it was at the time" do
430
+ expect(@book1.authors).to(eq([]))
431
+ end
432
+ end
433
+ end
434
+
435
+ context "and then the associated is dissociated between model versions" do
436
+ before do
437
+ @book.authors = []
438
+ Timecop.travel(1.second.since)
439
+ @book.update_attributes(title: "book_2")
440
+ end
441
+
442
+ context "when reified" do
443
+ before { @book1 = @book.versions.last.reify(has_many: true) }
444
+
445
+ it "see the associated as it was at the time" do
446
+ expect(@book1.authors).to(eq([]))
447
+ end
448
+ end
449
+ end
450
+
451
+ context "and then another associated is created" do
452
+ before { @book.authors.create!(name: "author_1") }
453
+
454
+ context "when reified" do
455
+ before { @book0 = @book.versions.last.reify(has_many: true) }
456
+
457
+ it "only see the first associated" do
458
+ expect(@book0.authors.map(&:name)).to(eq(["author_0"]))
459
+ end
460
+
461
+ it "not persist changes to the live association" do
462
+ expect(@book.authors.reload.map(&:name)).to(eq(%w[author_0 author_1]))
463
+ end
464
+ end
465
+
466
+ context "when reified with option mark_for_destruction" do
467
+ before do
468
+ @book0 = @book.versions.last.reify(has_many: true, mark_for_destruction: true)
469
+ end
470
+
471
+ it "mark the newly associated for destruction" do
472
+ author = @book0.authors.detect { |a| a.name == "author_1" }
473
+ expect(author).to be_marked_for_destruction
474
+ end
475
+
476
+ it "mark the newly associated-through for destruction" do
477
+ authorship = @book0.authorships.detect { |as| as.author.name == "author_1" }
478
+ expect(authorship).to be_marked_for_destruction
479
+ end
480
+ end
481
+ end
482
+
483
+ context "and then an existing one is associated" do
484
+ before { (@book.authors << @person_existing) }
485
+
486
+ context "when reified" do
487
+ before { @book0 = @book.versions.last.reify(has_many: true) }
488
+
489
+ it "only see the first associated" do
490
+ expect(@book0.authors.map(&:name)).to(eq(["author_0"]))
491
+ end
492
+
493
+ it "not persist changes to the live association" do
494
+ expect(@book.authors.reload.map(&:name).sort).to(eq(%w[author_0 person_existing]))
495
+ end
496
+ end
497
+
498
+ context "when reified with option mark_for_destruction" do
499
+ before do
500
+ @book0 = @book.versions.last.reify(has_many: true, mark_for_destruction: true)
501
+ end
502
+
503
+ it "not mark the newly associated for destruction" do
504
+ author = @book0.authors.detect { |a| a.name == "person_existing" }
505
+ expect(author).not_to be_marked_for_destruction
506
+ end
507
+
508
+ it "mark the newly associated-through for destruction" do
509
+ authorship = @book0.authorships.detect { |as| as.author.name == "person_existing" }
510
+ expect(authorship).to be_marked_for_destruction
511
+ end
512
+ end
513
+ end
514
+ end
515
+
516
+ context "updated before the associated without paper_trail was created" do
517
+ before do
518
+ @book.update_attributes!(title: "book_1")
519
+ @book.editors.create!(name: "editor_0")
520
+ end
521
+
522
+ context "when reified" do
523
+ before { @book0 = @book.versions.last.reify(has_many: true) }
524
+
525
+ it "see the live association" do
526
+ expect(@book0.editors.map(&:name)).to(eq(["editor_0"]))
527
+ end
528
+ end
529
+ end
530
+ end
531
+
532
+ context "Chapters, Sections, Paragraphs, Quotations, and Citations" do
533
+ before { @chapter = Chapter.create(name: CHAPTER_NAMES[0]) }
534
+
535
+ context "before any associations are created" do
536
+ before { @chapter.update_attributes(name: CHAPTER_NAMES[1]) }
537
+
538
+ it "not reify any associations" do
539
+ chapter_v1 = @chapter.versions[1].reify(has_many: true)
540
+ expect(chapter_v1.name).to(eq(CHAPTER_NAMES[0]))
541
+ expect(chapter_v1.sections).to(eq([]))
542
+ expect(chapter_v1.paragraphs).to(eq([]))
543
+ end
544
+ end
545
+
546
+ context "after the first has_many through relationship is created" do
547
+ before do
548
+ expect(@chapter.versions.size).to(eq(1))
549
+ @chapter.update_attributes(name: CHAPTER_NAMES[1])
550
+ expect(@chapter.versions.size).to(eq(2))
551
+ Timecop.travel(1.second.since)
552
+ @chapter.sections.create(name: "section 1")
553
+ Timecop.travel(1.second.since)
554
+ @chapter.sections.first.update_attributes(name: "section 2")
555
+ Timecop.travel(1.second.since)
556
+ @chapter.update_attributes(name: CHAPTER_NAMES[2])
557
+ expect(@chapter.versions.size).to(eq(3))
558
+ Timecop.travel(1.second.since)
559
+ @chapter.sections.first.update_attributes(name: "section 3")
560
+ end
561
+
562
+ context "version 1" do
563
+ it "have no sections" do
564
+ chapter_v1 = @chapter.versions[1].reify(has_many: true)
565
+ expect(chapter_v1.sections).to(eq([]))
566
+ end
567
+ end
568
+
569
+ context "version 2" do
570
+ it "have one section" do
571
+ chapter_v2 = @chapter.versions[2].reify(has_many: true)
572
+ expect(chapter_v2.sections.size).to(eq(1))
573
+ expect(chapter_v2.sections.map(&:name)).to(eq(["section 2"]))
574
+ expect(chapter_v2.name).to(eq(CHAPTER_NAMES[1]))
575
+ end
576
+ end
577
+
578
+ context "version 2, before the section was destroyed" do
579
+ before do
580
+ @chapter.update_attributes(name: CHAPTER_NAMES[2])
581
+ Timecop.travel(1.second.since)
582
+ @chapter.sections.destroy_all
583
+ Timecop.travel(1.second.since)
584
+ end
585
+
586
+ it "have the one section" do
587
+ chapter_v2 = @chapter.versions[2].reify(has_many: true)
588
+ expect(chapter_v2.sections.map(&:name)).to(eq(["section 2"]))
589
+ end
590
+ end
591
+
592
+ context "version 3, after the section was destroyed" do
593
+ before do
594
+ @chapter.sections.destroy_all
595
+ Timecop.travel(1.second.since)
596
+ @chapter.update_attributes(name: CHAPTER_NAMES[3])
597
+ Timecop.travel(1.second.since)
598
+ end
599
+
600
+ it "have no sections" do
601
+ chapter_v3 = @chapter.versions[3].reify(has_many: true)
602
+ expect(chapter_v3.sections.size).to(eq(0))
603
+ end
604
+ end
605
+
606
+ context "after creating a paragraph" do
607
+ before do
608
+ expect(@chapter.versions.size).to(eq(3))
609
+ @section = @chapter.sections.first
610
+ Timecop.travel(1.second.since)
611
+ @paragraph = @section.paragraphs.create(name: "para1")
612
+ end
613
+
614
+ context "new chapter version" do
615
+ it "have one paragraph" do
616
+ initial_section_name = @section.name
617
+ initial_paragraph_name = @paragraph.name
618
+ Timecop.travel(1.second.since)
619
+ @chapter.update_attributes(name: CHAPTER_NAMES[4])
620
+ expect(@chapter.versions.size).to(eq(4))
621
+ Timecop.travel(1.second.since)
622
+ @paragraph.update_attributes(name: "para3")
623
+ chapter_v3 = @chapter.versions[3].reify(has_many: true)
624
+ expect(chapter_v3.sections.map(&:name)).to(eq([initial_section_name]))
625
+ paragraphs = chapter_v3.sections.first.paragraphs
626
+ expect(paragraphs.size).to(eq(1))
627
+ expect(paragraphs.map(&:name)).to(eq([initial_paragraph_name]))
628
+ end
629
+ end
630
+
631
+ context "the version before a section is destroyed" do
632
+ it "have the section and paragraph" do
633
+ Timecop.travel(1.second.since)
634
+ @chapter.update_attributes(name: CHAPTER_NAMES[3])
635
+ expect(@chapter.versions.size).to(eq(4))
636
+ Timecop.travel(1.second.since)
637
+ @section.destroy
638
+ expect(@chapter.versions.size).to(eq(4))
639
+ chapter_v3 = @chapter.versions[3].reify(has_many: true)
640
+ expect(chapter_v3.name).to(eq(CHAPTER_NAMES[2]))
641
+ expect(chapter_v3.sections).to(eq([@section]))
642
+ expect(chapter_v3.sections[0].paragraphs).to(eq([@paragraph]))
643
+ expect(chapter_v3.paragraphs).to(eq([@paragraph]))
644
+ end
645
+ end
646
+
647
+ context "the version after a section is destroyed" do
648
+ it "not have any sections or paragraphs" do
649
+ @section.destroy
650
+ Timecop.travel(1.second.since)
651
+ @chapter.update_attributes(name: CHAPTER_NAMES[5])
652
+ expect(@chapter.versions.size).to(eq(4))
653
+ chapter_v3 = @chapter.versions[3].reify(has_many: true)
654
+ expect(chapter_v3.sections.size).to(eq(0))
655
+ expect(chapter_v3.paragraphs.size).to(eq(0))
656
+ end
657
+ end
658
+
659
+ context "the version before a paragraph is destroyed" do
660
+ it "have the one paragraph" do
661
+ initial_paragraph_name = @section.paragraphs.first.name
662
+ Timecop.travel(1.second.since)
663
+ @chapter.update_attributes(name: CHAPTER_NAMES[5])
664
+ Timecop.travel(1.second.since)
665
+ @paragraph.destroy
666
+ chapter_v3 = @chapter.versions[3].reify(has_many: true)
667
+ paragraphs = chapter_v3.sections.first.paragraphs
668
+ expect(paragraphs.size).to(eq(1))
669
+ expect(paragraphs.first.name).to(eq(initial_paragraph_name))
670
+ end
671
+ end
672
+
673
+ context "the version after a paragraph is destroyed" do
674
+ it "have no paragraphs" do
675
+ @paragraph.destroy
676
+ Timecop.travel(1.second.since)
677
+ @chapter.update_attributes(name: CHAPTER_NAMES[5])
678
+ chapter_v3 = @chapter.versions[3].reify(has_many: true)
679
+ expect(chapter_v3.paragraphs.size).to(eq(0))
680
+ expect(chapter_v3.sections.first.paragraphs).to(eq([]))
681
+ end
682
+ end
683
+ end
684
+ end
685
+
686
+ context "a chapter with one paragraph and one citation" do
687
+ it "reify paragraphs and citations" do
688
+ chapter = Chapter.create(name: CHAPTER_NAMES[0])
689
+ section = Section.create(name: "Section One", chapter: chapter)
690
+ paragraph = Paragraph.create(name: "Paragraph One", section: section)
691
+ quotation = Quotation.create(chapter: chapter)
692
+ citation = Citation.create(quotation: quotation)
693
+ Timecop.travel(1.second.since)
694
+ chapter.update_attributes(name: CHAPTER_NAMES[1])
695
+ expect(chapter.versions.count).to(eq(2))
696
+ paragraph.destroy
697
+ citation.destroy
698
+ reified = chapter.versions[1].reify(has_many: true)
699
+ expect(reified.sections.first.paragraphs).to(eq([paragraph]))
700
+ expect(reified.quotations.first.citations).to(eq([citation]))
701
+ end
702
+ end
703
+ end
704
+ end
705
+
706
+ context "belongs_to associations" do
707
+ context "Wotsit and Widget" do
708
+ before { @widget = Widget.create(name: "widget_0") }
709
+
710
+ context "where the association is created between model versions" do
711
+ before do
712
+ @wotsit = Wotsit.create(name: "wotsit_0")
713
+ Timecop.travel(1.second.since)
714
+ @wotsit.update_attributes(widget_id: @widget.id, name: "wotsit_1")
715
+ end
716
+
717
+ context "when reified" do
718
+ before { @wotsit0 = @wotsit.versions.last.reify(belongs_to: true) }
719
+
720
+ it "see the associated as it was at the time" do
721
+ expect(@wotsit0.widget).to be_nil
722
+ end
723
+
724
+ it "not persist changes to the live association" do
725
+ expect(@wotsit.reload.widget).to(eq(@widget))
726
+ end
727
+ end
728
+
729
+ context "and then the associated is updated between model versions" do
730
+ before do
731
+ @widget.update_attributes(name: "widget_1")
732
+ @widget.update_attributes(name: "widget_2")
733
+ Timecop.travel(1.second.since)
734
+ @wotsit.update_attributes(name: "wotsit_2")
735
+ @widget.update_attributes(name: "widget_3")
736
+ end
737
+
738
+ context "when reified" do
739
+ before { @wotsit1 = @wotsit.versions.last.reify(belongs_to: true) }
740
+
741
+ it "see the associated as it was at the time" do
742
+ expect(@wotsit1.widget.name).to(eq("widget_2"))
743
+ end
744
+
745
+ it "not persist changes to the live association" do
746
+ expect(@wotsit.reload.widget.name).to(eq("widget_3"))
747
+ end
748
+ end
749
+
750
+ context "when reified opting out of belongs_to reification" do
751
+ before { @wotsit1 = @wotsit.versions.last.reify(belongs_to: false) }
752
+
753
+ it "see the associated as it is live" do
754
+ expect(@wotsit1.widget.name).to(eq("widget_3"))
755
+ end
756
+ end
757
+ end
758
+
759
+ context "and then the associated is destroyed" do
760
+ before do
761
+ @wotsit.update_attributes(name: "wotsit_2")
762
+ @widget.destroy
763
+ end
764
+
765
+ context "when reified with belongs_to: true" do
766
+ before { @wotsit2 = @wotsit.versions.last.reify(belongs_to: true) }
767
+
768
+ it "see the associated as it was at the time" do
769
+ expect(@wotsit2.widget).to(eq(@widget))
770
+ end
771
+
772
+ it "not persist changes to the live association" do
773
+ expect(@wotsit.reload.widget).to be_nil
774
+ end
775
+
776
+ it "be able to persist the reified record" do
777
+ expect { @wotsit2.save! }.not_to(raise_error)
778
+ end
779
+ end
780
+
781
+ context "when reified with belongs_to: false" do
782
+ before { @wotsit2 = @wotsit.versions.last.reify(belongs_to: false) }
783
+
784
+ it "save should not re-create the widget record" do
785
+ @wotsit2.save!
786
+ expect(::Widget.find_by(id: @widget.id)).to be_nil
787
+ end
788
+ end
789
+
790
+ context "and then the model is updated" do
791
+ before do
792
+ Timecop.travel(1.second.since)
793
+ @wotsit.update_attributes(name: "wotsit_3")
794
+ end
795
+
796
+ context "when reified" do
797
+ before { @wotsit2 = @wotsit.versions.last.reify(belongs_to: true) }
798
+
799
+ it "see the associated as it was the time" do
800
+ expect(@wotsit2.widget).to be_nil
801
+ end
802
+ end
803
+ end
804
+ end
805
+ end
806
+
807
+ context "where the association is changed between model versions" do
808
+ before do
809
+ @wotsit = @widget.create_wotsit(name: "wotsit_0")
810
+ Timecop.travel(1.second.since)
811
+ @new_widget = Widget.create(name: "new_widget")
812
+ @wotsit.update_attributes(widget_id: @new_widget.id, name: "wotsit_1")
813
+ end
814
+
815
+ context "when reified" do
816
+ before { @wotsit0 = @wotsit.versions.last.reify(belongs_to: true) }
817
+
818
+ it "see the association as it was at the time" do
819
+ expect(@wotsit0.widget.name).to(eq("widget_0"))
820
+ end
821
+
822
+ it "not persist changes to the live association" do
823
+ expect(@wotsit.reload.widget).to(eq(@new_widget))
824
+ end
825
+ end
826
+
827
+ context "when reified with option mark_for_destruction" do
828
+ before do
829
+ @wotsit0 = @wotsit.versions.last.reify(belongs_to: true, mark_for_destruction: true)
830
+ end
831
+
832
+ it "does not mark the new associated for destruction" do
833
+ expect(@new_widget.marked_for_destruction?).to(eq(false))
834
+ end
835
+ end
836
+ end
837
+ end
838
+ end
839
+
840
+ context "has_and_belongs_to_many associations" do
841
+ context "foo and bar" do
842
+ before do
843
+ @foo = FooHabtm.create(name: "foo")
844
+ Timecop.travel(1.second.since)
845
+ end
846
+
847
+ context "where the association is created between model versions" do
848
+ before do
849
+ @foo.update_attributes(name: "foo1", bar_habtms: [BarHabtm.create(name: "bar")])
850
+ end
851
+
852
+ context "when reified" do
853
+ before do
854
+ @reified = @foo.versions.last.reify(has_and_belongs_to_many: true)
855
+ end
856
+
857
+ it "see the associated as it was at the time" do
858
+ expect(@reified.bar_habtms.length).to(eq(0))
859
+ end
860
+
861
+ it "not persist changes to the live association" do
862
+ expect(@foo.reload.bar_habtms).not_to(eq(@reified.bar_habtms))
863
+ end
864
+ end
865
+ end
866
+
867
+ context "where the association is changed between model versions" do
868
+ before do
869
+ @foo.update_attributes(name: "foo2", bar_habtms: [BarHabtm.create(name: "bar2")])
870
+ Timecop.travel(1.second.since)
871
+ @foo.update_attributes(name: "foo3", bar_habtms: [BarHabtm.create(name: "bar3")])
872
+ end
873
+
874
+ context "when reified" do
875
+ before do
876
+ @reified = @foo.versions.last.reify(has_and_belongs_to_many: true)
877
+ end
878
+
879
+ it "see the association as it was at the time" do
880
+ expect(@reified.bar_habtms.first.name).to(eq("bar2"))
881
+ end
882
+
883
+ it "not persist changes to the live association" do
884
+ expect(@foo.reload.bar_habtms.first).not_to(eq(@reified.bar_habtms.first))
885
+ end
886
+ end
887
+
888
+ context "when reified with has_and_belongs_to_many: false" do
889
+ before { @reified = @foo.versions.last.reify }
890
+
891
+ it "see the association as it is now" do
892
+ expect(@reified.bar_habtms.first.name).to(eq("bar3"))
893
+ end
894
+ end
895
+ end
896
+
897
+ context "where the association is destroyed between model versions" do
898
+ before do
899
+ @foo.update_attributes(name: "foo2", bar_habtms: [BarHabtm.create(name: "bar2")])
900
+ Timecop.travel(1.second.since)
901
+ @foo.update_attributes(name: "foo3", bar_habtms: [])
902
+ end
903
+
904
+ context "when reified" do
905
+ before do
906
+ @reified = @foo.versions.last.reify(has_and_belongs_to_many: true)
907
+ end
908
+
909
+ it "see the association as it was at the time" do
910
+ expect(@reified.bar_habtms.first.name).to(eq("bar2"))
911
+ end
912
+
913
+ it "not persist changes to the live association" do
914
+ expect(@foo.reload.bar_habtms.first).not_to(eq(@reified.bar_habtms.first))
915
+ end
916
+ end
917
+ end
918
+
919
+ context "where the unassociated model changes" do
920
+ before do
921
+ @bar = BarHabtm.create(name: "bar2")
922
+ @foo.update_attributes(name: "foo2", bar_habtms: [@bar])
923
+ Timecop.travel(1.second.since)
924
+ @foo.update_attributes(name: "foo3", bar_habtms: [BarHabtm.create(name: "bar4")])
925
+ Timecop.travel(1.second.since)
926
+ @bar.update_attributes(name: "bar3")
927
+ end
928
+
929
+ context "when reified" do
930
+ before do
931
+ @reified = @foo.versions.last.reify(has_and_belongs_to_many: true)
932
+ end
933
+
934
+ it "see the association as it was at the time" do
935
+ expect(@reified.bar_habtms.first.name).to(eq("bar2"))
936
+ end
937
+
938
+ it "not persist changes to the live association" do
939
+ expect(@foo.reload.bar_habtms.first).not_to(eq(@reified.bar_habtms.first))
940
+ end
941
+ end
942
+ end
943
+ end
944
+
945
+ context "updated via nested attributes" do
946
+ before do
947
+ @foo = FooHabtm.create(name: "foo", bar_habtms_attributes: [{ name: "bar" }])
948
+ Timecop.travel(1.second.since)
949
+ @foo.update_attributes(
950
+ name: "foo2",
951
+ bar_habtms_attributes: [{ id: @foo.bar_habtms.first.id, name: "bar2" }]
952
+ )
953
+ @reified = @foo.versions.last.reify(has_and_belongs_to_many: true)
954
+ end
955
+
956
+ it "see the associated object as it was at the time" do
957
+ expect(@reified.bar_habtms.first.name).to(eq("bar"))
958
+ end
959
+
960
+ it "not persist changes to the live object" do
961
+ expect(@foo.reload.bar_habtms.first.name).not_to(eq(@reified.bar_habtms.first.name))
962
+ end
963
+ end
964
+ end
965
+ end