paper_trail_without_deprecated 3.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +21 -0
  5. data/CHANGELOG.md +68 -0
  6. data/Gemfile +2 -0
  7. data/MIT-LICENSE +20 -0
  8. data/README.md +979 -0
  9. data/Rakefile +18 -0
  10. data/gemfiles/3.0.gemfile +31 -0
  11. data/lib/generators/paper_trail/USAGE +2 -0
  12. data/lib/generators/paper_trail/install_generator.rb +23 -0
  13. data/lib/generators/paper_trail/templates/add_object_changes_column_to_versions.rb +9 -0
  14. data/lib/generators/paper_trail/templates/create_versions.rb +18 -0
  15. data/lib/paper_trail.rb +115 -0
  16. data/lib/paper_trail/cleaner.rb +34 -0
  17. data/lib/paper_trail/config.rb +14 -0
  18. data/lib/paper_trail/frameworks/cucumber.rb +31 -0
  19. data/lib/paper_trail/frameworks/rails.rb +79 -0
  20. data/lib/paper_trail/frameworks/rspec.rb +24 -0
  21. data/lib/paper_trail/frameworks/rspec/extensions.rb +20 -0
  22. data/lib/paper_trail/frameworks/sinatra.rb +31 -0
  23. data/lib/paper_trail/has_paper_trail.rb +308 -0
  24. data/lib/paper_trail/serializers/json.rb +17 -0
  25. data/lib/paper_trail/serializers/yaml.rb +17 -0
  26. data/lib/paper_trail/version.rb +200 -0
  27. data/lib/paper_trail/version_number.rb +3 -0
  28. data/paper_trail.gemspec +36 -0
  29. data/spec/models/widget_spec.rb +13 -0
  30. data/spec/paper_trail_spec.rb +47 -0
  31. data/spec/spec_helper.rb +41 -0
  32. data/test/custom_json_serializer.rb +13 -0
  33. data/test/dummy/Rakefile +7 -0
  34. data/test/dummy/app/controllers/application_controller.rb +17 -0
  35. data/test/dummy/app/controllers/test_controller.rb +5 -0
  36. data/test/dummy/app/controllers/widgets_controller.rb +31 -0
  37. data/test/dummy/app/helpers/application_helper.rb +2 -0
  38. data/test/dummy/app/models/animal.rb +4 -0
  39. data/test/dummy/app/models/article.rb +16 -0
  40. data/test/dummy/app/models/authorship.rb +5 -0
  41. data/test/dummy/app/models/book.rb +5 -0
  42. data/test/dummy/app/models/cat.rb +2 -0
  43. data/test/dummy/app/models/document.rb +4 -0
  44. data/test/dummy/app/models/dog.rb +2 -0
  45. data/test/dummy/app/models/elephant.rb +3 -0
  46. data/test/dummy/app/models/fluxor.rb +3 -0
  47. data/test/dummy/app/models/foo_widget.rb +2 -0
  48. data/test/dummy/app/models/legacy_widget.rb +4 -0
  49. data/test/dummy/app/models/person.rb +28 -0
  50. data/test/dummy/app/models/post.rb +4 -0
  51. data/test/dummy/app/models/protected_widget.rb +3 -0
  52. data/test/dummy/app/models/song.rb +12 -0
  53. data/test/dummy/app/models/translation.rb +4 -0
  54. data/test/dummy/app/models/widget.rb +10 -0
  55. data/test/dummy/app/models/wotsit.rb +4 -0
  56. data/test/dummy/app/versions/post_version.rb +3 -0
  57. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  58. data/test/dummy/config.ru +4 -0
  59. data/test/dummy/config/application.rb +63 -0
  60. data/test/dummy/config/boot.rb +10 -0
  61. data/test/dummy/config/database.yml +22 -0
  62. data/test/dummy/config/environment.rb +5 -0
  63. data/test/dummy/config/environments/development.rb +40 -0
  64. data/test/dummy/config/environments/production.rb +73 -0
  65. data/test/dummy/config/environments/test.rb +37 -0
  66. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  67. data/test/dummy/config/initializers/inflections.rb +10 -0
  68. data/test/dummy/config/initializers/mime_types.rb +5 -0
  69. data/test/dummy/config/initializers/paper_trail.rb +5 -0
  70. data/test/dummy/config/initializers/secret_token.rb +7 -0
  71. data/test/dummy/config/initializers/session_store.rb +8 -0
  72. data/test/dummy/config/locales/en.yml +5 -0
  73. data/test/dummy/config/routes.rb +3 -0
  74. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +136 -0
  75. data/test/dummy/db/schema.rb +101 -0
  76. data/test/dummy/public/404.html +26 -0
  77. data/test/dummy/public/422.html +26 -0
  78. data/test/dummy/public/500.html +26 -0
  79. data/test/dummy/public/favicon.ico +0 -0
  80. data/test/dummy/public/javascripts/application.js +2 -0
  81. data/test/dummy/public/javascripts/controls.js +965 -0
  82. data/test/dummy/public/javascripts/dragdrop.js +974 -0
  83. data/test/dummy/public/javascripts/effects.js +1123 -0
  84. data/test/dummy/public/javascripts/prototype.js +6001 -0
  85. data/test/dummy/public/javascripts/rails.js +175 -0
  86. data/test/dummy/public/stylesheets/.gitkeep +0 -0
  87. data/test/dummy/script/rails +6 -0
  88. data/test/functional/controller_test.rb +90 -0
  89. data/test/functional/modular_sinatra_test.rb +44 -0
  90. data/test/functional/sinatra_test.rb +45 -0
  91. data/test/functional/thread_safety_test.rb +26 -0
  92. data/test/paper_trail_test.rb +27 -0
  93. data/test/test_helper.rb +40 -0
  94. data/test/unit/cleaner_test.rb +143 -0
  95. data/test/unit/inheritance_column_test.rb +43 -0
  96. data/test/unit/model_test.rb +1314 -0
  97. data/test/unit/protected_attrs_test.rb +46 -0
  98. data/test/unit/serializer_test.rb +117 -0
  99. data/test/unit/serializers/json_test.rb +40 -0
  100. data/test/unit/serializers/mixin_json_test.rb +36 -0
  101. data/test/unit/serializers/mixin_yaml_test.rb +49 -0
  102. data/test/unit/serializers/yaml_test.rb +40 -0
  103. data/test/unit/timestamp_test.rb +44 -0
  104. data/test/unit/version_test.rb +74 -0
  105. metadata +286 -0
@@ -0,0 +1,143 @@
1
+ require 'test_helper'
2
+
3
+ class PaperTrailCleanerTest < ActiveSupport::TestCase
4
+
5
+ setup do
6
+ @animals = [@animal = Animal.new, @dog = Dog.new, @cat = Cat.new]
7
+ @animals.each do |animal|
8
+ 3.times { animal.update_attribute(:name, Faker::Name.name) }
9
+ end
10
+ end
11
+
12
+ test 'Baseline' do
13
+ assert_equal 9, PaperTrail::Version.count
14
+ @animals.each { |animal| assert_equal 3, animal.versions.size }
15
+ end
16
+
17
+ context '`clean_versions!` method' do
18
+ should 'be extended by `PaperTrail` module' do
19
+ assert_respond_to PaperTrail, :clean_versions!
20
+ end
21
+
22
+ context 'No options provided' do
23
+ should 'removes extra versions for each item' do
24
+ PaperTrail.clean_versions!
25
+ assert_equal 3, PaperTrail::Version.count
26
+ @animals.each { |animal| assert_equal 1, animal.versions.size }
27
+ end
28
+
29
+ should 'removes the earliest version(s)' do
30
+ most_recent_version_names = @animals.map { |animal| animal.versions.last.reify.name }
31
+ PaperTrail.clean_versions!
32
+ assert_equal most_recent_version_names, @animals.map { |animal| animal.versions.last.reify.name }
33
+ end
34
+ end
35
+
36
+ context '`:keeping` option' do
37
+ should 'modifies the number of versions ommitted from destruction' do
38
+ PaperTrail.clean_versions!(:keeping => 2)
39
+ assert_equal 6, PaperTrail::Version.all.count
40
+ @animals.each { |animal| assert_equal 2, animal.versions.size }
41
+ end
42
+ end
43
+
44
+ context '`:date` option' do
45
+ setup do
46
+ @animal.versions.each { |ver| ver.update_attribute(:created_at, ver.created_at - 1.day) }
47
+ @date = @animal.versions.first.created_at.to_date
48
+ @animal.update_attribute(:name, Faker::Name.name)
49
+ end
50
+
51
+ should 'restrict the versions destroyed to those that were created on the date provided' do
52
+ assert_equal 10, PaperTrail::Version.count
53
+ assert_equal 4, @animal.versions.size
54
+ assert_equal 3, @animal.versions_between(@date, @date + 1.day).size
55
+ PaperTrail.clean_versions!(:date => @date)
56
+ assert_equal 8, PaperTrail::Version.count
57
+ assert_equal 2, @animal.versions(true).size
58
+ assert_equal @date, @animal.versions.first.created_at.to_date
59
+ assert_not_same @date, @animal.versions.last.created_at.to_date
60
+ end
61
+ end
62
+
63
+ context '`:item_id` option' do
64
+ context 'single ID received' do
65
+ should 'restrict the versions destroyed to the versions for the Item with that ID' do
66
+ PaperTrail.clean_versions!(:item_id => @animal.id)
67
+ assert_equal 1, @animal.versions.size
68
+ assert_equal 7, PaperTrail::Version.count
69
+ end
70
+ end
71
+
72
+ context "collection of ID's received" do
73
+ should "restrict the versions destroyed to the versions for the Item with those ID's" do
74
+ PaperTrail.clean_versions!(:item_id => [@animal.id, @dog.id])
75
+ assert_equal 1, @animal.versions.size
76
+ assert_equal 1, @dog.versions.size
77
+ assert_equal 5, PaperTrail::Version.count
78
+ end
79
+ end
80
+ end
81
+
82
+ context 'options combinations' do # additional tests to cover combinations of options
83
+ context '`:date`' do
84
+ setup do
85
+ [@animal, @dog].each do |animal|
86
+ animal.versions.each { |ver| ver.update_attribute(:created_at, ver.created_at - 1.day) }
87
+ animal.update_attribute(:name, Faker::Name.name)
88
+ end
89
+ @date = @animal.versions.first.created_at.to_date
90
+ end
91
+
92
+ should 'Baseline' do
93
+ assert_equal 11, PaperTrail::Version.count
94
+ [@animal, @dog].each do |animal|
95
+ assert_equal 4, animal.versions.size
96
+ assert_equal 3, animal.versions.between(@date, @date+1.day).size
97
+ end
98
+ end
99
+
100
+ context 'and `:keeping`' do
101
+ should 'restrict cleaning properly' do
102
+ PaperTrail.clean_versions!(:date => @date, :keeping => 2)
103
+ [@animal, @dog].each do |animal|
104
+ animal.versions.reload # reload the association to pick up the destructions made by the `Cleaner`
105
+ assert_equal 3, animal.versions.size
106
+ assert_equal 2, animal.versions.between(@date, @date+1.day).size
107
+ end
108
+ assert_equal 9, PaperTrail::Version.count # ensure that the versions for the `@cat` instance wasn't touched
109
+ end
110
+ end
111
+
112
+ context 'and `:item_id`' do
113
+ should 'restrict cleaning properly' do
114
+ PaperTrail.clean_versions!(:date => @date, :item_id => @dog.id)
115
+ @dog.versions.reload # reload the association to pick up the destructions made by the `Cleaner`
116
+ assert_equal 2, @dog.versions.size
117
+ assert_equal 1, @dog.versions.between(@date, @date+1.day).size
118
+ assert_equal 9, PaperTrail::Version.count # ensure the versions for other animals besides `@animal` weren't touched
119
+ end
120
+ end
121
+
122
+ context ', `:item_id`, and `:keeping`' do
123
+ should 'restrict cleaning properly' do
124
+ PaperTrail.clean_versions!(:date => @date, :item_id => @dog.id, :keeping => 2)
125
+ @dog.versions.reload # reload the association to pick up the destructions made by the `Cleaner`
126
+ assert_equal 3, @dog.versions.size
127
+ assert_equal 2, @dog.versions.between(@date, @date+1.day).size
128
+ assert_equal 10, PaperTrail::Version.count # ensure the versions for other animals besides `@animal` weren't touched
129
+ end
130
+ end
131
+ end
132
+
133
+ context '`:keeping` and `:item_id`' do
134
+ should 'restrict cleaning properly' do
135
+ PaperTrail.clean_versions!(:keeping => 2, :item_id => @animal.id)
136
+ assert_equal 2, @animal.versions.size
137
+ assert_equal 8, PaperTrail::Version.count # ensure the versions for other animals besides `@animal` weren't touched
138
+ end
139
+ end
140
+ end
141
+
142
+ end # clean_versions! method
143
+ end
@@ -0,0 +1,43 @@
1
+ require 'test_helper'
2
+
3
+ class InheritanceColumnTest < ActiveSupport::TestCase
4
+
5
+ context 'STI models' do
6
+ setup do
7
+ @animal = Animal.create :name => 'Animal'
8
+ @animal.update_attributes :name => 'Animal from the Muppets'
9
+ @animal.update_attributes :name => 'Animal Muppet'
10
+ @animal.destroy
11
+
12
+ @dog = Dog.create :name => 'Snoopy'
13
+ @dog.update_attributes :name => 'Scooby'
14
+ @dog.update_attributes :name => 'Scooby Doo'
15
+ @dog.destroy
16
+
17
+ @cat = Cat.create :name => 'Garfield'
18
+ @cat.update_attributes :name => 'Garfield (I hate Mondays)'
19
+ @cat.update_attributes :name => 'Garfield The Cat'
20
+ @cat.destroy
21
+ end
22
+
23
+ should 'work with custom STI inheritance column' do
24
+ assert_equal 12, PaperTrail::Version.count
25
+ assert_equal 4, @animal.versions.count
26
+ assert @animal.versions.first.reify.nil?
27
+ @animal.versions[1..-1].each { |v| assert_equal 'Animal', v.reify.class.name }
28
+
29
+ # For some reason `@dog.versions` doesn't include the final `destroy` version.
30
+ # Neither do `@dog.versions.scoped` nor `@dog.versions(true)` nor `@dog.versions.reload`.
31
+ dog_versions = PaperTrail::Version.where(:item_id => @dog.id)
32
+ assert_equal 4, dog_versions.count
33
+ assert dog_versions.first.reify.nil?
34
+ dog_versions[1..-1].each { |v| assert_equal 'Dog', v.reify.class.name }
35
+
36
+ cat_versions = PaperTrail::Version.where(:item_id => @cat.id)
37
+ assert_equal 4, cat_versions.count
38
+ assert cat_versions.first.reify.nil?
39
+ cat_versions[1..-1].each { |v| assert_equal 'Cat', v.reify.class.name }
40
+ end
41
+ end
42
+
43
+ end
@@ -0,0 +1,1314 @@
1
+ require 'test_helper'
2
+
3
+ class HasPaperTrailModelTest < ActiveSupport::TestCase
4
+
5
+ context "A record with defined 'only' and 'ignore' attributes" do
6
+ setup { @article = Article.create }
7
+ should 'creation should change the number of versions' do assert_equal(1, PaperTrail::Version.count) end
8
+
9
+ context 'which updates an ignored column' do
10
+ setup { @article.update_attributes :title => 'My first title' }
11
+ should 'not change the number of versions' do assert_equal(1, PaperTrail::Version.count) end
12
+ end
13
+
14
+ context 'which updates an ignored column and a selected column' do
15
+ setup { @article.update_attributes :title => 'My first title', :content => 'Some text here.' }
16
+ should 'change the number of versions' do assert_equal(2, PaperTrail::Version.count) end
17
+
18
+ should "show the new version in the model's `versions` association" do
19
+ assert_equal(2, @article.versions.size)
20
+ end
21
+
22
+ should 'have stored only non-ignored attributes' do
23
+ assert_equal ({'content' => [nil, 'Some text here.']}), @article.versions.last.changeset
24
+ end
25
+ end
26
+
27
+ context 'which updates a selected column' do
28
+ setup { @article.update_attributes :content => 'Some text here.' }
29
+ should 'change the number of versions' do assert_equal(2, PaperTrail::Version.count) end
30
+
31
+ should "show the new version in the model's `versions` association" do
32
+ assert_equal(2, @article.versions.size)
33
+ end
34
+ end
35
+
36
+ context 'which updates a non-ignored and non-selected column' do
37
+ setup { @article.update_attributes :abstract => 'Other abstract'}
38
+ should 'not change the number of versions' do assert_equal(1, PaperTrail::Version.count) end
39
+ end
40
+
41
+ context 'which updates a skipped column' do
42
+ setup { @article.update_attributes :file_upload => 'Your data goes here' }
43
+ should 'not change the number of versions' do assert_equal(1, PaperTrail::Version.count) end
44
+ end
45
+
46
+ context 'which updates a skipped column and a selected column' do
47
+ setup { @article.update_attributes :file_upload => 'Your data goes here', :content => 'Some text here.' }
48
+ should 'change the number of versions' do assert_equal(2, PaperTrail::Version.count) end
49
+
50
+ should "show the new version in the model's `versions` association" do
51
+ assert_equal(2, @article.versions.size)
52
+ end
53
+
54
+ should 'have stored only non-skipped attributes' do
55
+ assert_equal ({'content' => [nil, 'Some text here.']}), @article.versions.last.changeset
56
+ end
57
+
58
+ context 'and when updated again' do
59
+ setup do
60
+ @article.update_attributes :file_upload => 'More data goes here', :content => 'More text here.'
61
+ @old_article = @article.versions.last
62
+ end
63
+
64
+ should 'have removed the skipped attributes when saving the previous version' do
65
+ assert_equal nil, PaperTrail.serializer.load(@old_article.object)['file_upload']
66
+ end
67
+
68
+ should 'have kept the non-skipped attributes in the previous version' do
69
+ assert_equal 'Some text here.', PaperTrail.serializer.load(@old_article.object)['content']
70
+ end
71
+ end
72
+ end
73
+
74
+ context 'which gets destroyed' do
75
+ setup { @article.destroy }
76
+ should 'change the number of versions' do assert_equal(2, PaperTrail::Version.count) end
77
+
78
+ should "show the new version in the model's `versions` association" do
79
+ assert_equal(2, @article.versions.size)
80
+ end
81
+ end
82
+ end
83
+
84
+ context "A record with defined 'ignore' attribute" do
85
+ setup { @legacy_widget = LegacyWidget.create }
86
+
87
+ context 'which updates an ignored column' do
88
+ setup { @legacy_widget.update_attributes :version => 1 }
89
+ should 'not change the number of versions' do assert_equal(1, PaperTrail::Version.count) end
90
+ end
91
+ end
92
+
93
+ context 'A record with defined "if" and "unless" attributes' do
94
+ setup { @translation = Translation.new :headline => 'Headline' }
95
+
96
+ context 'for non-US translations' do
97
+ setup { @translation.save }
98
+ should 'not change the number of versions' do assert_equal(0, PaperTrail::Version.count) end
99
+
100
+ context 'after update' do
101
+ setup { @translation.update_attributes :content => 'Content' }
102
+ should 'not change the number of versions' do assert_equal(0, PaperTrail::Version.count) end
103
+ end
104
+
105
+ context 'after destroy' do
106
+ setup { @translation.destroy }
107
+ should 'not change the number of versions' do assert_equal(0, PaperTrail::Version.count) end
108
+ end
109
+ end
110
+
111
+ context 'for US translations' do
112
+ setup { @translation.language_code = "US" }
113
+
114
+ context 'that are drafts' do
115
+ setup do
116
+ @translation.type = 'DRAFT'
117
+ @translation.save
118
+ end
119
+
120
+ should 'not change the number of versions' do assert_equal(0, PaperTrail::Version.count) end
121
+
122
+ context 'after update' do
123
+ setup { @translation.update_attributes :content => 'Content' }
124
+ should 'not change the number of versions' do assert_equal(0, PaperTrail::Version.count) end
125
+ end
126
+ end
127
+
128
+ context 'that are not drafts' do
129
+ setup { @translation.save }
130
+
131
+ should 'change the number of versions' do assert_equal(1, PaperTrail::Version.count) end
132
+
133
+ context 'after update' do
134
+ setup { @translation.update_attributes :content => 'Content' }
135
+ should 'change the number of versions' do assert_equal(2, PaperTrail::Version.count) end
136
+
137
+ should "show the new version in the model's `versions` association" do
138
+ assert_equal(2, @translation.versions.size)
139
+ end
140
+ end
141
+
142
+ context 'after destroy' do
143
+ setup { @translation.destroy }
144
+ should 'change the number of versions' do assert_equal(2, PaperTrail::Version.count) end
145
+
146
+ should "show the new version in the model's `versions` association" do
147
+ assert_equal(2, @translation.versions.size)
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ context 'A new record' do
155
+ setup { @widget = Widget.new }
156
+
157
+ should 'not have any previous versions' do
158
+ assert_equal [], @widget.versions
159
+ end
160
+
161
+ should 'be live' do
162
+ assert @widget.live?
163
+ end
164
+
165
+
166
+ context 'which is then created' do
167
+ setup { @widget.update_attributes :name => 'Henry' }
168
+
169
+ should 'have one previous version' do
170
+ assert_equal 1, @widget.versions.length
171
+ end
172
+
173
+ should 'be nil in its previous version' do
174
+ assert_nil @widget.versions.first.object
175
+ assert_nil @widget.versions.first.reify
176
+ end
177
+
178
+ should 'record the correct event' do
179
+ assert_match /create/i, @widget.versions.first.event
180
+ end
181
+
182
+ should 'be live' do
183
+ assert @widget.live?
184
+ end
185
+
186
+ should 'have changes' do
187
+ changes = {
188
+ 'name' => [nil, 'Henry'],
189
+ 'created_at' => [nil, @widget.created_at],
190
+ 'updated_at' => [nil, @widget.updated_at],
191
+ 'id' => [nil, 1]
192
+ }
193
+
194
+ assert_equal changes, @widget.versions.last.changeset
195
+ end
196
+
197
+ context 'and then updated without any changes' do
198
+ setup { @widget.touch }
199
+
200
+ should 'not have a new version' do
201
+ assert_equal 1, @widget.versions.length
202
+ end
203
+ end
204
+
205
+
206
+ context 'and then updated with changes' do
207
+ setup { @widget.update_attributes :name => 'Harry' }
208
+
209
+ should 'have two previous versions' do
210
+ assert_equal 2, @widget.versions.length
211
+ end
212
+
213
+ should 'be available in its previous version' do
214
+ assert_equal 'Harry', @widget.name
215
+ assert_not_nil @widget.versions.last.object
216
+ widget = @widget.versions.last.reify
217
+ assert_equal 'Henry', widget.name
218
+ assert_equal 'Harry', @widget.name
219
+ end
220
+
221
+ should 'have the same ID in its previous version' do
222
+ assert_equal @widget.id, @widget.versions.last.reify.id
223
+ end
224
+
225
+ should 'record the correct event' do
226
+ assert_match /update/i, @widget.versions.last.event
227
+ end
228
+
229
+ should 'have versions that are not live' do
230
+ assert @widget.versions.map(&:reify).compact.all? { |w| !w.live? }
231
+ end
232
+
233
+ should 'have stored changes' do
234
+ # Behavior for ActiveRecord 4 is different than ActiveRecord 3;
235
+ # AR4 includes the `updated_at` column in changes for updates, which is why we reject it from the right side of this assertion.
236
+ assert_equal ({'name' => ['Henry', 'Harry']}), PaperTrail.serializer.load(@widget.versions.last.object_changes).reject { |k,v| k.to_sym == :updated_at }
237
+ assert_equal ({'name' => ['Henry', 'Harry']}), @widget.versions.last.changeset.reject { |k,v| k.to_sym == :updated_at }
238
+ end
239
+
240
+ should 'return changes with indifferent access' do
241
+ assert_equal ['Henry', 'Harry'], @widget.versions.last.changeset[:name]
242
+ assert_equal ['Henry', 'Harry'], @widget.versions.last.changeset['name']
243
+ end
244
+
245
+ if defined?(ActiveRecord::IdentityMap) && ActiveRecord::IdentityMap.respond_to?(:without)
246
+ should 'not clobber the IdentityMap when reifying' do
247
+ module ActiveRecord::IdentityMap
248
+ class << self
249
+ alias :__without :without
250
+ def without(&block)
251
+ @unclobbered = true
252
+ __without(&block)
253
+ end
254
+ end
255
+ end
256
+
257
+ @widget.versions.last.reify
258
+ assert ActiveRecord::IdentityMap.instance_variable_get("@unclobbered")
259
+ end
260
+ end
261
+
262
+ context 'and has one associated object' do
263
+ setup do
264
+ @wotsit = @widget.create_wotsit :name => 'John'
265
+ end
266
+
267
+ should 'not copy the has_one association by default when reifying' do
268
+ reified_widget = @widget.versions.last.reify
269
+ assert_equal @wotsit, reified_widget.wotsit # association hasn't been affected by reifying
270
+ assert_equal @wotsit, @widget.wotsit # confirm that the association is correct
271
+ end
272
+
273
+ should 'copy the has_one association when reifying with :has_one => true' do
274
+ reified_widget = @widget.versions.last.reify(:has_one => true)
275
+ assert_nil reified_widget.wotsit # wotsit wasn't there at the last version
276
+ assert_equal @wotsit, @widget.wotsit # wotsit came into being on the live object
277
+ end
278
+ end
279
+
280
+
281
+ context 'and has many associated objects' do
282
+ setup do
283
+ @f0 = @widget.fluxors.create :name => 'f-zero'
284
+ @f1 = @widget.fluxors.create :name => 'f-one'
285
+ @reified_widget = @widget.versions.last.reify
286
+ end
287
+
288
+ should 'copy the has_many associations when reifying' do
289
+ assert_equal @widget.fluxors.length, @reified_widget.fluxors.length
290
+ assert_same_elements @widget.fluxors, @reified_widget.fluxors
291
+
292
+ assert_equal @widget.versions.length, @reified_widget.versions.length
293
+ assert_same_elements @widget.versions, @reified_widget.versions
294
+ end
295
+ end
296
+
297
+
298
+ context 'and then destroyed' do
299
+ setup do
300
+ @fluxor = @widget.fluxors.create :name => 'flux'
301
+ @widget.destroy
302
+ @reified_widget = PaperTrail::Version.last.reify
303
+ end
304
+
305
+ should 'record the correct event' do
306
+ assert_match /destroy/i, PaperTrail::Version.last.event
307
+ end
308
+
309
+ should 'have three previous versions' do
310
+ assert_equal 3, PaperTrail::Version.with_item_keys('Widget', @widget.id).length
311
+ end
312
+
313
+ should 'be available in its previous version' do
314
+ assert_equal @widget.id, @reified_widget.id
315
+ assert_equal @widget.attributes, @reified_widget.attributes
316
+ end
317
+
318
+ should 'be re-creatable from its previous version' do
319
+ assert @reified_widget.save
320
+ end
321
+
322
+ should 'restore its associations on its previous version' do
323
+ @reified_widget.save
324
+ assert_equal 1, @reified_widget.fluxors.length
325
+ end
326
+
327
+ should 'not have changes' do
328
+ assert_equal Hash.new, @widget.versions.last.changeset
329
+ end
330
+ end
331
+ end
332
+ end
333
+ end
334
+
335
+
336
+ # Test the serialisation and deserialisation.
337
+ # TODO: binary
338
+ context "A record's papertrail" do
339
+ setup do
340
+ @date_time = DateTime.now.utc
341
+ @time = Time.now
342
+ @date = Date.new 2009, 5, 29
343
+ @widget = Widget.create :name => 'Warble',
344
+ :a_text => 'The quick brown fox',
345
+ :an_integer => 42,
346
+ :a_float => 153.01,
347
+ :a_decimal => 2.71828,
348
+ :a_datetime => @date_time,
349
+ :a_time => @time,
350
+ :a_date => @date,
351
+ :a_boolean => true
352
+
353
+ @widget.update_attributes :name => nil,
354
+ :a_text => nil,
355
+ :an_integer => nil,
356
+ :a_float => nil,
357
+ :a_decimal => nil,
358
+ :a_datetime => nil,
359
+ :a_time => nil,
360
+ :a_date => nil,
361
+ :a_boolean => false
362
+ @previous = @widget.versions.last.reify
363
+ end
364
+
365
+ should 'handle strings' do
366
+ assert_equal 'Warble', @previous.name
367
+ end
368
+
369
+ should 'handle text' do
370
+ assert_equal 'The quick brown fox', @previous.a_text
371
+ end
372
+
373
+ should 'handle integers' do
374
+ assert_equal 42, @previous.an_integer
375
+ end
376
+
377
+ should 'handle floats' do
378
+ assert_in_delta 153.01, @previous.a_float, 0.001
379
+ end
380
+
381
+ should 'handle decimals' do
382
+ assert_in_delta 2.71828, @previous.a_decimal, 0.00001
383
+ end
384
+
385
+ should 'handle datetimes' do
386
+ assert_equal @date_time.to_time.utc.to_i, @previous.a_datetime.to_time.utc.to_i
387
+ end
388
+
389
+ should 'handle times' do
390
+ assert_equal @time.utc.to_i, @previous.a_time.utc.to_i
391
+ end
392
+
393
+ should 'handle dates' do
394
+ assert_equal @date, @previous.a_date
395
+ end
396
+
397
+ should 'handle booleans' do
398
+ assert @previous.a_boolean
399
+ end
400
+
401
+
402
+ context "after a column is removed from the record's schema" do
403
+ setup do
404
+ change_schema
405
+ Widget.connection.schema_cache.clear!
406
+ Widget.reset_column_information
407
+ assert_raise(NoMethodError) { Widget.new.sacrificial_column }
408
+ @last = @widget.versions.last
409
+ end
410
+
411
+ should 'reify previous version' do
412
+ assert_kind_of Widget, @last.reify
413
+ end
414
+
415
+ should 'restore all forward-compatible attributes' do
416
+ assert_equal 'Warble', @last.reify.name
417
+ assert_equal 'The quick brown fox', @last.reify.a_text
418
+ assert_equal 42, @last.reify.an_integer
419
+ assert_in_delta 153.01, @last.reify.a_float, 0.001
420
+ assert_in_delta 2.71828, @last.reify.a_decimal, 0.00001
421
+ assert_equal @date_time.to_time.utc.to_i, @last.reify.a_datetime.to_time.utc.to_i
422
+ assert_equal @time.utc.to_i, @last.reify.a_time.utc.to_i
423
+ assert_equal @date, @last.reify.a_date
424
+ assert @last.reify.a_boolean
425
+ end
426
+ end
427
+ end
428
+
429
+
430
+ context 'A record' do
431
+ setup { @widget = Widget.create :name => 'Zaphod' }
432
+
433
+ context 'with PaperTrail globally disabled' do
434
+ setup do
435
+ PaperTrail.enabled = false
436
+ @count = @widget.versions.length
437
+ end
438
+
439
+ teardown { PaperTrail.enabled = true }
440
+
441
+ context 'when updated' do
442
+ setup { @widget.update_attributes :name => 'Beeblebrox' }
443
+
444
+ should 'not add to its trail' do
445
+ assert_equal @count, @widget.versions.length
446
+ end
447
+ end
448
+ end
449
+
450
+ context 'with its paper trail turned off' do
451
+ setup do
452
+ Widget.paper_trail_off
453
+ @count = @widget.versions.length
454
+ end
455
+
456
+ teardown { Widget.paper_trail_on }
457
+
458
+ context 'when updated' do
459
+ setup { @widget.update_attributes :name => 'Beeblebrox' }
460
+
461
+ should 'not add to its trail' do
462
+ assert_equal @count, @widget.versions.length
463
+ end
464
+ end
465
+
466
+ context 'when destroyed "without versioning"' do
467
+ should 'leave paper trail off after call' do
468
+ @widget.without_versioning :destroy
469
+ assert !Widget.paper_trail_enabled_for_model
470
+ end
471
+ end
472
+
473
+ context 'and then its paper trail turned on' do
474
+ setup { Widget.paper_trail_on }
475
+
476
+ context 'when updated' do
477
+ setup { @widget.update_attributes :name => 'Ford' }
478
+
479
+ should 'add to its trail' do
480
+ assert_equal @count + 1, @widget.versions.length
481
+ end
482
+ end
483
+
484
+ context 'when updated "without versioning"' do
485
+ setup do
486
+ @widget.without_versioning do
487
+ @widget.update_attributes :name => 'Ford'
488
+ end
489
+ end
490
+
491
+ should 'not create new version' do
492
+ assert_equal 1, @widget.versions.length
493
+ end
494
+
495
+ should 'enable paper trail after call' do
496
+ assert Widget.paper_trail_enabled_for_model
497
+ end
498
+ end
499
+ end
500
+ end
501
+ end
502
+
503
+
504
+ context 'A papertrail with somebody making changes' do
505
+ setup do
506
+ @widget = Widget.new :name => 'Fidget'
507
+ end
508
+
509
+ context 'when a record is created' do
510
+ setup do
511
+ PaperTrail.whodunnit = 'Alice'
512
+ @widget.save
513
+ @version = @widget.versions.last # only 1 version
514
+ end
515
+
516
+ should 'track who made the change' do
517
+ assert_equal 'Alice', @version.whodunnit
518
+ assert_nil @version.originator
519
+ assert_equal 'Alice', @version.terminator
520
+ assert_equal 'Alice', @widget.originator
521
+ end
522
+
523
+ context 'when a record is updated' do
524
+ setup do
525
+ PaperTrail.whodunnit = 'Bob'
526
+ @widget.update_attributes :name => 'Rivet'
527
+ @version = @widget.versions.last
528
+ end
529
+
530
+ should 'track who made the change' do
531
+ assert_equal 'Bob', @version.whodunnit
532
+ assert_equal 'Alice', @version.originator
533
+ assert_equal 'Bob', @version.terminator
534
+ assert_equal 'Bob', @widget.originator
535
+ end
536
+
537
+ context 'when a record is destroyed' do
538
+ setup do
539
+ PaperTrail.whodunnit = 'Charlie'
540
+ @widget.destroy
541
+ @version = PaperTrail::Version.last
542
+ end
543
+
544
+ should 'track who made the change' do
545
+ assert_equal 'Charlie', @version.whodunnit
546
+ assert_equal 'Bob', @version.originator
547
+ assert_equal 'Charlie', @version.terminator
548
+ assert_equal 'Charlie', @widget.originator
549
+ end
550
+ end
551
+ end
552
+ end
553
+ end
554
+
555
+
556
+ context 'A subclass' do
557
+ setup do
558
+ @foo = FooWidget.create
559
+ @foo.update_attributes! :name => 'Foo'
560
+ end
561
+
562
+ should 'reify with the correct type' do
563
+ # For some reason this test appears to be broken on AR4 in the test env. Executing it manually in the Rails console seems to work.. not sure what the issues is here.
564
+ assert_kind_of FooWidget, @foo.versions.last.reify if ActiveRecord::VERSION::STRING.to_f < 4.0
565
+ assert_equal @foo.versions.first, PaperTrail::Version.last.previous
566
+ assert_nil PaperTrail::Version.last.next
567
+ end
568
+
569
+ should 'should return the correct originator' do
570
+ PaperTrail.whodunnit = 'Ben'
571
+ @foo.update_attribute(:name, 'Geoffrey')
572
+ assert_equal PaperTrail.whodunnit, @foo.originator
573
+ end
574
+
575
+ context 'when destroyed' do
576
+ setup { @foo.destroy }
577
+
578
+ should 'reify with the correct type' do
579
+ assert_kind_of FooWidget, @foo.versions.last.reify
580
+ assert_equal @foo.versions[1], PaperTrail::Version.last.previous
581
+ assert_nil PaperTrail::Version.last.next
582
+ end
583
+ end
584
+ end
585
+
586
+
587
+ context 'An item with versions' do
588
+ setup do
589
+ @widget = Widget.create :name => 'Widget'
590
+ @widget.update_attributes :name => 'Fidget'
591
+ @widget.update_attributes :name => 'Digit'
592
+ end
593
+
594
+ context 'which were created over time' do
595
+ setup do
596
+ @created = 2.days.ago
597
+ @first_update = 1.day.ago
598
+ @second_update = 1.hour.ago
599
+ @widget.versions[0].update_attributes :created_at => @created
600
+ @widget.versions[1].update_attributes :created_at => @first_update
601
+ @widget.versions[2].update_attributes :created_at => @second_update
602
+ @widget.update_attribute :updated_at, @second_update
603
+ end
604
+
605
+ should 'return nil for version_at before it was created' do
606
+ assert_nil @widget.version_at(@created - 1)
607
+ end
608
+
609
+ should 'return how it looked when created for version_at its creation' do
610
+ assert_equal 'Widget', @widget.version_at(@created).name
611
+ end
612
+
613
+ should "return how it looked when created for version_at just before its first update" do
614
+ assert_equal 'Widget', @widget.version_at(@first_update - 1).name
615
+ end
616
+
617
+ should "return how it looked when first updated for version_at its first update" do
618
+ assert_equal 'Fidget', @widget.version_at(@first_update).name
619
+ end
620
+
621
+ should 'return how it looked when first updated for version_at just before its second update' do
622
+ assert_equal 'Fidget', @widget.version_at(@second_update - 1).name
623
+ end
624
+
625
+ should 'return how it looked when subsequently updated for version_at its second update' do
626
+ assert_equal 'Digit', @widget.version_at(@second_update).name
627
+ end
628
+
629
+ should 'return the current object for version_at after latest update' do
630
+ assert_equal 'Digit', @widget.version_at(1.day.from_now).name
631
+ end
632
+
633
+ context 'passing in a string representation of a timestamp' do
634
+ should 'still return a widget when appropriate' do
635
+ # need to add 1 second onto the timestamps before casting to a string, since casting a Time to a string drops the microseconds
636
+ assert_equal 'Widget', @widget.version_at((@created + 1.second).to_s).name
637
+ assert_equal 'Fidget', @widget.version_at((@first_update + 1.second).to_s).name
638
+ assert_equal 'Digit', @widget.version_at((@second_update + 1.second).to_s).name
639
+ end
640
+ end
641
+ end
642
+
643
+ context '.versions_between' do
644
+ setup do
645
+ @created = 30.days.ago
646
+ @first_update = 15.days.ago
647
+ @second_update = 1.day.ago
648
+ @widget.versions[0].update_attributes :created_at => @created
649
+ @widget.versions[1].update_attributes :created_at => @first_update
650
+ @widget.versions[2].update_attributes :created_at => @second_update
651
+ @widget.update_attribute :updated_at, @second_update
652
+ end
653
+
654
+ should 'return versions in the time period' do
655
+ assert_equal ['Fidget'], @widget.versions_between(20.days.ago, 10.days.ago).map(&:name)
656
+ assert_equal ['Widget', 'Fidget'], @widget.versions_between(45.days.ago, 10.days.ago).map(&:name)
657
+ assert_equal ['Fidget', 'Digit'], @widget.versions_between(16.days.ago, 1.minute.ago).map(&:name)
658
+ assert_equal [], @widget.versions_between(60.days.ago, 45.days.ago).map(&:name)
659
+ end
660
+ end
661
+
662
+ context 'on the first version' do
663
+ setup { @version = @widget.versions.first }
664
+
665
+ should 'have a nil previous version' do
666
+ assert_nil @version.previous
667
+ end
668
+
669
+ should 'return the next version' do
670
+ assert_equal @widget.versions[1], @version.next
671
+ end
672
+
673
+ should 'return the correct index' do
674
+ assert_equal 0, @version.index
675
+ end
676
+ end
677
+
678
+ context 'on the last version' do
679
+ setup { @version = @widget.versions.last }
680
+
681
+ should 'return the previous version' do
682
+ assert_equal @widget.versions[@widget.versions.length - 2], @version.previous
683
+ end
684
+
685
+ should 'have a nil next version' do
686
+ assert_nil @version.next
687
+ end
688
+
689
+ should 'return the correct index' do
690
+ assert_equal @widget.versions.length - 1, @version.index
691
+ end
692
+ end
693
+ end
694
+
695
+
696
+ context 'An item' do
697
+ setup do
698
+ @initial_title = 'Foobar'
699
+ @article = Article.new :title => @initial_title
700
+ end
701
+
702
+ context 'which is created' do
703
+ setup { @article.save }
704
+
705
+ should 'store fixed meta data' do
706
+ assert_equal 42, @article.versions.last.answer
707
+ end
708
+
709
+ should 'store dynamic meta data which is independent of the item' do
710
+ assert_equal '31 + 11 = 42', @article.versions.last.question
711
+ end
712
+
713
+ should 'store dynamic meta data which depends on the item' do
714
+ assert_equal @article.id, @article.versions.last.article_id
715
+ end
716
+
717
+ should 'store dynamic meta data based on a method of the item' do
718
+ assert_equal @article.action_data_provider_method, @article.versions.last.action
719
+ end
720
+
721
+ should 'store dynamic meta data based on an attribute of the item prior to creation' do
722
+ assert_equal nil, @article.versions.last.title
723
+ end
724
+
725
+
726
+ context 'and updated' do
727
+ setup do
728
+ @article.update_attributes! :content => 'Better text.', :title => 'Rhubarb'
729
+ end
730
+
731
+ should 'store fixed meta data' do
732
+ assert_equal 42, @article.versions.last.answer
733
+ end
734
+
735
+ should 'store dynamic meta data which is independent of the item' do
736
+ assert_equal '31 + 11 = 42', @article.versions.last.question
737
+ end
738
+
739
+ should 'store dynamic meta data which depends on the item' do
740
+ assert_equal @article.id, @article.versions.last.article_id
741
+ end
742
+
743
+ should 'store dynamic meta data based on an attribute of the item prior to the update' do
744
+ assert_equal @initial_title, @article.versions.last.title
745
+ end
746
+ end
747
+
748
+
749
+ context 'and destroyed' do
750
+ setup { @article.destroy }
751
+
752
+ should 'store fixed meta data' do
753
+ assert_equal 42, @article.versions.last.answer
754
+ end
755
+
756
+ should 'store dynamic meta data which is independent of the item' do
757
+ assert_equal '31 + 11 = 42', @article.versions.last.question
758
+ end
759
+
760
+ should 'store dynamic meta data which depends on the item' do
761
+ assert_equal @article.id, @article.versions.last.article_id
762
+ end
763
+
764
+ should 'store dynamic meta data based on an attribute of the item prior to the destruction' do
765
+ assert_equal @initial_title, @article.versions.last.title
766
+ end
767
+ end
768
+ end
769
+ end
770
+
771
+ context 'A reified item' do
772
+ setup do
773
+ widget = Widget.create :name => 'Bob'
774
+ %w( Tom Dick Jane ).each { |name| widget.update_attributes :name => name }
775
+ @version = widget.versions.last
776
+ @widget = @version.reify
777
+ end
778
+
779
+ should 'know which version it came from' do
780
+ assert_equal @version, @widget.version
781
+ end
782
+
783
+ should 'return its previous self' do
784
+ assert_equal @widget.versions[-2].reify, @widget.previous_version
785
+ end
786
+
787
+ end
788
+
789
+
790
+ context 'A non-reified item' do
791
+ setup { @widget = Widget.new }
792
+
793
+ should 'not have a previous version' do
794
+ assert_nil @widget.previous_version
795
+ end
796
+
797
+ should 'not have a next version' do
798
+ assert_nil @widget.next_version
799
+ end
800
+
801
+ context 'with versions' do
802
+ setup do
803
+ @widget.save
804
+ %w( Tom Dick Jane ).each { |name| @widget.update_attributes :name => name }
805
+ end
806
+
807
+ should 'have a previous version' do
808
+ assert_equal @widget.versions.last.reify.name, @widget.previous_version.name
809
+ end
810
+
811
+ should 'not have a next version' do
812
+ assert_nil @widget.next_version
813
+ end
814
+ end
815
+ end
816
+
817
+ context 'A reified item' do
818
+ setup do
819
+ @widget = Widget.create :name => 'Bob'
820
+ %w(Tom Dick Jane).each { |name| @widget.update_attributes :name => name }
821
+ @second_widget = @widget.versions[1].reify # first widget is `nil`
822
+ @last_widget = @widget.versions.last.reify
823
+ end
824
+
825
+ should 'have a previous version' do
826
+ assert_nil @second_widget.previous_version # `create` events return `nil` for `reify`
827
+ assert_equal @widget.versions[-2].reify.name, @last_widget.previous_version.name
828
+ end
829
+
830
+ should 'have a next version' do
831
+ assert_equal @widget.versions[2].reify.name, @second_widget.next_version.name
832
+ assert_equal @last_widget.next_version.name, @widget.name
833
+ end
834
+ end
835
+
836
+ context ":has_many :through" do
837
+ setup do
838
+ @book = Book.create :title => 'War and Peace'
839
+ @dostoyevsky = Person.create :name => 'Dostoyevsky'
840
+ @solzhenitsyn = Person.create :name => 'Solzhenitsyn'
841
+ end
842
+
843
+ should 'store version on source <<' do
844
+ count = PaperTrail::Version.count
845
+ @book.authors << @dostoyevsky
846
+ assert_equal 1, PaperTrail::Version.count - count
847
+ assert_equal PaperTrail::Version.last, @book.authorships.first.versions.first
848
+ end
849
+
850
+ should 'store version on source create' do
851
+ count = PaperTrail::Version.count
852
+ @book.authors.create :name => 'Tolstoy'
853
+ assert_equal 2, PaperTrail::Version.count - count
854
+ assert_same_elements [Person.last, Authorship.last], [PaperTrail::Version.all[-2].item, PaperTrail::Version.last.item]
855
+ end
856
+
857
+ should 'store version on join destroy' do
858
+ @book.authors << @dostoyevsky
859
+ count = PaperTrail::Version.count
860
+ @book.authorships(true).last.destroy
861
+ assert_equal 1, PaperTrail::Version.count - count
862
+ assert_equal @book, PaperTrail::Version.last.reify.book
863
+ assert_equal @dostoyevsky, PaperTrail::Version.last.reify.person
864
+ end
865
+
866
+ should 'store version on join clear' do
867
+ @book.authors << @dostoyevsky
868
+ count = PaperTrail::Version.count
869
+ @book.authorships(true).clear
870
+ assert_equal 1, PaperTrail::Version.count - count
871
+ assert_equal @book, PaperTrail::Version.last.reify.book
872
+ assert_equal @dostoyevsky, PaperTrail::Version.last.reify.person
873
+ end
874
+ end
875
+
876
+
877
+ context 'A model with a has_one association' do
878
+ setup { @widget = Widget.create :name => 'widget_0' }
879
+
880
+ context 'before the associated was created' do
881
+ setup do
882
+ @widget.update_attributes :name => 'widget_1'
883
+ @wotsit = @widget.create_wotsit :name => 'wotsit_0'
884
+ end
885
+
886
+ context 'when reified' do
887
+ setup { @widget_0 = @widget.versions.last.reify(:has_one => 1) }
888
+
889
+ should 'see the associated as it was at the time' do
890
+ assert_nil @widget_0.wotsit
891
+ end
892
+ end
893
+ end
894
+
895
+ context 'where the association is created between model versions' do
896
+ setup do
897
+ @wotsit = @widget.create_wotsit :name => 'wotsit_0'
898
+ make_last_version_earlier @wotsit
899
+
900
+ @widget.update_attributes :name => 'widget_1'
901
+ end
902
+
903
+ context 'when reified' do
904
+ setup { @widget_0 = @widget.versions.last.reify(:has_one => 1) }
905
+
906
+ should 'see the associated as it was at the time' do
907
+ assert_equal 'wotsit_0', @widget_0.wotsit.name
908
+ end
909
+ end
910
+
911
+ context 'and then the associated is updated between model versions' do
912
+ setup do
913
+ @wotsit.update_attributes :name => 'wotsit_1'
914
+ make_last_version_earlier @wotsit
915
+ @wotsit.update_attributes :name => 'wotsit_2'
916
+ make_last_version_earlier @wotsit
917
+
918
+ @widget.update_attributes :name => 'widget_2'
919
+ @wotsit.update_attributes :name => 'wotsit_3'
920
+ end
921
+
922
+ context 'when reified' do
923
+ setup { @widget_1 = @widget.versions.last.reify(:has_one => 1) }
924
+
925
+ should 'see the associated as it was at the time' do
926
+ assert_equal 'wotsit_2', @widget_1.wotsit.name
927
+ end
928
+ end
929
+
930
+ context 'when reified opting out of has_one reification' do
931
+ setup { @widget_1 = @widget.versions.last.reify(:has_one => false) }
932
+
933
+ should 'see the associated as it is live' do
934
+ assert_equal 'wotsit_3', @widget_1.wotsit.name
935
+ end
936
+ end
937
+ end
938
+
939
+ context 'and then the associated is destroyed between model versions' do
940
+ setup do
941
+ @wotsit.destroy
942
+ make_last_version_earlier @wotsit
943
+
944
+ @widget.update_attributes :name => 'widget_3'
945
+ end
946
+
947
+ context 'when reified' do
948
+ setup { @widget_2 = @widget.versions.last.reify(:has_one => 1) }
949
+
950
+ should 'see the associated as it was at the time' do
951
+ assert_nil @widget_2.wotsit
952
+ end
953
+ end
954
+ end
955
+ end
956
+ end
957
+
958
+ context 'When an attribute has a custom serializer' do
959
+ setup { @person = Person.new(:time_zone => "Samoa") }
960
+
961
+ should "be an instance of ActiveSupport::TimeZone" do
962
+ assert_equal ActiveSupport::TimeZone, @person.time_zone.class
963
+ end
964
+
965
+ context 'when the model is saved' do
966
+ setup do
967
+ @changes_before_save = @person.changes.dup
968
+ @person.save!
969
+ end
970
+
971
+ # Test for serialization:
972
+ should 'version.object_changes should not have stored the default, ridiculously long (to_yaml) serialization of the TimeZone object' do
973
+ assert @person.versions.last.object_changes.length < 105, "object_changes length was #{@person.versions.last.object_changes.length}"
974
+ end
975
+ # It should store the serialized value.
976
+ should 'version.object_changes attribute should have stored the value returned by the attribute serializer' do
977
+ as_stored_in_version = HashWithIndifferentAccess[YAML::load(@person.versions.last.object_changes)]
978
+ assert_equal [nil, 'Samoa'], as_stored_in_version[:time_zone]
979
+ assert_equal @person.instance_variable_get(:@attributes)['time_zone'].serialized_value, as_stored_in_version[:time_zone].last
980
+ end
981
+
982
+ # Tests for unserialization:
983
+ should 'version.changeset should convert the attribute value back to its original, unserialized value' do
984
+ assert_equal @person.instance_variable_get(:@attributes)['time_zone'].unserialized_value, @person.versions.last.changeset[:time_zone].last
985
+ end
986
+ should "record.changes (before save) returns the original, unserialized values" do
987
+ assert_equal [NilClass, ActiveSupport::TimeZone], @changes_before_save[:time_zone].map(&:class)
988
+ end
989
+ should 'version.changeset should be the same as record.changes was before the save' do
990
+ assert_equal @changes_before_save, @person.versions.last.changeset.delete_if { |key, val| key.to_sym == :id }
991
+ assert_equal [NilClass, ActiveSupport::TimeZone], @person.versions.last.changeset[:time_zone].map(&:class)
992
+ end
993
+
994
+ context 'when that attribute is updated' do
995
+ setup do
996
+ @attribute_value_before_change = @person.instance_variable_get(:@attributes)['time_zone']
997
+ @person.assign_attributes({ :time_zone => 'Pacific Time (US & Canada)' })
998
+ @changes_before_save = @person.changes.dup
999
+ @person.save!
1000
+ end
1001
+
1002
+ # Tests for serialization:
1003
+ # Before the serialized attributes fix, the object/object_changes value that was stored was ridiculously long (58723).
1004
+ should 'version.object should not have stored the default, ridiculously long (to_yaml) serialization of the TimeZone object' do
1005
+ assert @person.versions.last.object. length < 105, "object length was #{@person.versions.last.object .length}"
1006
+ end
1007
+ # Need an additional clause to detect what version of ActiveRecord is being used for this test because AR4 injects the `updated_at` column into the changeset for updates to models
1008
+ should 'version.object_changes should not have stored the default, ridiculously long (to_yaml) serialization of the TimeZone object' do
1009
+ assert @person.versions.last.object_changes.length < (ActiveRecord::VERSION::STRING.to_f < 4.0 ? 105 : 118), "object_changes length was #{@person.versions.last.object_changes.length}"
1010
+ end
1011
+ # But now it stores the short, serialized value.
1012
+ should 'version.object attribute should have stored the value returned by the attribute serializer' do
1013
+ as_stored_in_version = HashWithIndifferentAccess[YAML::load(@person.versions.last.object)]
1014
+ assert_equal 'Samoa', as_stored_in_version[:time_zone]
1015
+ assert_equal @attribute_value_before_change.serialized_value, as_stored_in_version[:time_zone]
1016
+ end
1017
+ should 'version.object_changes attribute should have stored the value returned by the attribute serializer' do
1018
+ as_stored_in_version = HashWithIndifferentAccess[YAML::load(@person.versions.last.object_changes)]
1019
+ assert_equal ['Samoa', 'Pacific Time (US & Canada)'], as_stored_in_version[:time_zone]
1020
+ assert_equal @person.instance_variable_get(:@attributes)['time_zone'].serialized_value, as_stored_in_version[:time_zone].last
1021
+ end
1022
+
1023
+ # Tests for unserialization:
1024
+ should 'version.reify should convert the attribute value back to its original, unserialized value' do
1025
+ assert_equal @attribute_value_before_change.unserialized_value, @person.versions.last.reify.time_zone
1026
+ end
1027
+ should 'version.changeset should convert the attribute value back to its original, unserialized value' do
1028
+ assert_equal @person.instance_variable_get(:@attributes)['time_zone'].unserialized_value, @person.versions.last.changeset[:time_zone].last
1029
+ end
1030
+ should "record.changes (before save) returns the original, unserialized values" do
1031
+ assert_equal [ActiveSupport::TimeZone, ActiveSupport::TimeZone], @changes_before_save[:time_zone].map(&:class)
1032
+ end
1033
+ should 'version.changeset should be the same as record.changes was before the save' do
1034
+ assert_equal @changes_before_save, @person.versions.last.changeset
1035
+ assert_equal [ActiveSupport::TimeZone, ActiveSupport::TimeZone], @person.versions.last.changeset[:time_zone].map(&:class)
1036
+ end
1037
+
1038
+ end
1039
+ end
1040
+ end
1041
+
1042
+
1043
+ context 'A new model instance which uses a custom PaperTrail::Version class' do
1044
+ setup { @post = Post.new }
1045
+
1046
+ context 'which is then saved' do
1047
+ setup { @post.save }
1048
+ should 'change the number of post versions' do assert_equal 1, PostVersion.count end
1049
+ should 'not change the number of versions' do assert_equal(0, PaperTrail::Version.count) end
1050
+ end
1051
+ end
1052
+
1053
+ context 'An existing model instance which uses a custom PaperTrail::Version class' do
1054
+ setup { @post = Post.create }
1055
+ should 'have one post version' do assert_equal(1, PostVersion.count) end
1056
+
1057
+ context 'on the first version' do
1058
+ setup { @version = @post.versions.first }
1059
+
1060
+ should 'have the correct index' do
1061
+ assert_equal 0, @version.index
1062
+ end
1063
+ end
1064
+
1065
+ should 'have versions of the custom class' do
1066
+ assert_equal "PostVersion", @post.versions.first.class.name
1067
+ end
1068
+
1069
+ context 'which is modified' do
1070
+ setup { @post.update_attributes({ :content => "Some new content" }) }
1071
+ should 'change the number of post versions' do assert_equal(2, PostVersion.count) end
1072
+ should 'not change the number of versions' do assert_equal(0, PaperTrail::Version.count) end
1073
+ should "not have stored changes when object_changes column doesn't exist" do
1074
+ assert_nil @post.versions.last.changeset
1075
+ end
1076
+ end
1077
+ end
1078
+
1079
+
1080
+ context 'An overwritten default accessor' do
1081
+ setup do
1082
+ @song = Song.create :length => 4
1083
+ @song.update_attributes :length => 5
1084
+ end
1085
+
1086
+ should 'return "overwritten" value on live instance' do
1087
+ assert_equal 5, @song.length
1088
+ end
1089
+ should 'return "overwritten" value on reified instance' do
1090
+ assert_equal 4, @song.versions.last.reify.length
1091
+ end
1092
+ end
1093
+
1094
+
1095
+ context 'An unsaved record' do
1096
+ setup do
1097
+ @widget = Widget.new
1098
+ @widget.destroy
1099
+ end
1100
+ should 'not have a version created on destroy' do
1101
+ assert @widget.versions.empty?
1102
+ end
1103
+ end
1104
+
1105
+ context 'A model with a custom association' do
1106
+ setup do
1107
+ @doc = Document.create
1108
+ @doc.update_attributes :name => 'Doc 1'
1109
+ end
1110
+
1111
+ should 'not respond to versions method' do
1112
+ assert !@doc.respond_to?(:versions)
1113
+ end
1114
+
1115
+ should 'create a new version record' do
1116
+ assert_equal 2, @doc.paper_trail_versions.length
1117
+ end
1118
+
1119
+ should 'respond to `next_version` as normal' do
1120
+ assert_equal @doc.paper_trail_versions.last.reify.next_version.name, @doc.name
1121
+ end
1122
+
1123
+ should 'respond to `previous_version` as normal' do
1124
+ @doc.update_attributes :name => 'Doc 2'
1125
+ assert_equal 3, @doc.paper_trail_versions.length
1126
+ assert_equal 'Doc 1', @doc.previous_version.name
1127
+ end
1128
+ end
1129
+
1130
+ context 'The `on` option' do
1131
+ context 'on create' do
1132
+ setup do
1133
+ Fluxor.instance_eval <<-END
1134
+ has_paper_trail :on => [:create]
1135
+ END
1136
+ @fluxor = Fluxor.create
1137
+ @fluxor.update_attributes :name => 'blah'
1138
+ @fluxor.destroy
1139
+ end
1140
+ should 'only have a version for the create event' do
1141
+ assert_equal 1, @fluxor.versions.length
1142
+ assert_equal 'create', @fluxor.versions.last.event
1143
+ end
1144
+ end
1145
+ context 'on update' do
1146
+ setup do
1147
+ Fluxor.reset_callbacks :create
1148
+ Fluxor.reset_callbacks :update
1149
+ Fluxor.reset_callbacks :destroy
1150
+ Fluxor.instance_eval <<-END
1151
+ has_paper_trail :on => [:update]
1152
+ END
1153
+ @fluxor = Fluxor.create
1154
+ @fluxor.update_attributes :name => 'blah'
1155
+ @fluxor.destroy
1156
+ end
1157
+ should 'only have a version for the update event' do
1158
+ assert_equal 1, @fluxor.versions.length
1159
+ assert_equal 'update', @fluxor.versions.last.event
1160
+ end
1161
+ end
1162
+ context 'on destroy' do
1163
+ setup do
1164
+ Fluxor.reset_callbacks :create
1165
+ Fluxor.reset_callbacks :update
1166
+ Fluxor.reset_callbacks :destroy
1167
+ Fluxor.instance_eval <<-END
1168
+ has_paper_trail :on => [:destroy]
1169
+ END
1170
+ @fluxor = Fluxor.create
1171
+ @fluxor.update_attributes :name => 'blah'
1172
+ @fluxor.destroy
1173
+ end
1174
+ should 'only have a version for the destroy event' do
1175
+ assert_equal 1, @fluxor.versions.length
1176
+ assert_equal 'destroy', @fluxor.versions.last.event
1177
+ end
1178
+ end
1179
+ context 'allows a symbol to be passed' do
1180
+ setup do
1181
+ Fluxor.reset_callbacks :create
1182
+ Fluxor.reset_callbacks :update
1183
+ Fluxor.reset_callbacks :destroy
1184
+ Fluxor.instance_eval <<-END
1185
+ has_paper_trail :on => :create
1186
+ END
1187
+ @fluxor = Fluxor.create
1188
+ @fluxor.update_attributes :name => 'blah'
1189
+ @fluxor.destroy
1190
+ end
1191
+ should 'only have a version for hte create event' do
1192
+ assert_equal 1, @fluxor.versions.length
1193
+ assert_equal 'create', @fluxor.versions.last.event
1194
+ end
1195
+ end
1196
+ end
1197
+
1198
+ context 'A model with column version and custom version_method' do
1199
+ setup do
1200
+ @legacy_widget = LegacyWidget.create(:name => "foo", :version => 2)
1201
+ end
1202
+
1203
+ should 'set version on create' do
1204
+ assert_equal 2, @legacy_widget.version
1205
+ end
1206
+
1207
+ should 'allow version updates' do
1208
+ @legacy_widget.update_attributes :version => 3
1209
+ assert_equal 3, @legacy_widget.version
1210
+ end
1211
+
1212
+ should 'create a new version record' do
1213
+ assert_equal 1, @legacy_widget.versions.size
1214
+ end
1215
+ end
1216
+
1217
+ context 'A reified item with a column -version- and custom version_method' do
1218
+ setup do
1219
+ widget = LegacyWidget.create(:name => "foo", :version => 2)
1220
+ %w( bar baz ).each { |name| widget.update_attributes :name => name }
1221
+ @version = widget.versions.last
1222
+ @widget = @version.reify
1223
+ end
1224
+
1225
+ should 'know which version it came from' do
1226
+ assert_equal @version, @widget.custom_version
1227
+ end
1228
+
1229
+ should 'return its previous self' do
1230
+ assert_equal @widget.versions[-2].reify, @widget.previous_version
1231
+ end
1232
+ end
1233
+
1234
+ context 'custom events' do
1235
+ context 'on create' do
1236
+ setup do
1237
+ Fluxor.reset_callbacks :create
1238
+ Fluxor.reset_callbacks :update
1239
+ Fluxor.reset_callbacks :destroy
1240
+ Fluxor.instance_eval <<-END
1241
+ has_paper_trail :on => [:create]
1242
+ END
1243
+ @fluxor = Fluxor.new.tap { |model| model.paper_trail_event = 'created' }
1244
+ @fluxor.update_attributes :name => 'blah'
1245
+ @fluxor.destroy
1246
+ end
1247
+ should 'only have a version for the created event' do
1248
+ assert_equal 1, @fluxor.versions.length
1249
+ assert_equal 'created', @fluxor.versions.last.event
1250
+ end
1251
+ end
1252
+ context 'on update' do
1253
+ setup do
1254
+ Fluxor.reset_callbacks :create
1255
+ Fluxor.reset_callbacks :update
1256
+ Fluxor.reset_callbacks :destroy
1257
+ Fluxor.instance_eval <<-END
1258
+ has_paper_trail :on => [:update]
1259
+ END
1260
+ @fluxor = Fluxor.create.tap { |model| model.paper_trail_event = 'name_updated' }
1261
+ @fluxor.update_attributes :name => 'blah'
1262
+ @fluxor.destroy
1263
+ end
1264
+ should 'only have a version for the name_updated event' do
1265
+ assert_equal 1, @fluxor.versions.length
1266
+ assert_equal 'name_updated', @fluxor.versions.last.event
1267
+ end
1268
+ end
1269
+ context 'on destroy' do
1270
+ setup do
1271
+ Fluxor.reset_callbacks :create
1272
+ Fluxor.reset_callbacks :update
1273
+ Fluxor.reset_callbacks :destroy
1274
+ Fluxor.instance_eval <<-END
1275
+ has_paper_trail :on => [:destroy]
1276
+ END
1277
+ @fluxor = Fluxor.create.tap { |model| model.paper_trail_event = 'destroyed' }
1278
+ @fluxor.update_attributes :name => 'blah'
1279
+ @fluxor.destroy
1280
+ end
1281
+ should 'only have a version for the destroy event' do
1282
+ assert_equal 1, @fluxor.versions.length
1283
+ assert_equal 'destroyed', @fluxor.versions.last.event
1284
+ end
1285
+ end
1286
+ end
1287
+
1288
+ context '`PaperTrail::Config.version_limit` set' do
1289
+ setup do
1290
+ PaperTrail.config.version_limit = 2
1291
+ @widget = Widget.create! :name => 'Henry'
1292
+ 6.times { @widget.update_attribute(:name, Faker::Lorem.word) }
1293
+ end
1294
+
1295
+ teardown { PaperTrail.config.version_limit = nil }
1296
+
1297
+ should "limit the number of versions to 3 (2 plus the created at event)" do
1298
+ assert_equal 'create', @widget.versions.first.event
1299
+ assert_equal 3, @widget.versions.size
1300
+ end
1301
+ end
1302
+
1303
+
1304
+ private
1305
+
1306
+ # Updates `model`'s last version so it looks like the version was
1307
+ # created 2 seconds ago.
1308
+ def make_last_version_earlier(model)
1309
+ PaperTrail::Version.record_timestamps = false
1310
+ model.versions.last.update_attributes :created_at => 2.seconds.ago
1311
+ PaperTrail::Version.record_timestamps = true
1312
+ end
1313
+
1314
+ end