paper_trail 7.0.2 → 7.0.3

Sign up to get free protection for your applications and to get access to all the features.
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