amoeba 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/activerecord_head.yml +26 -0
  3. data/.github/workflows/jruby.yml +4 -1
  4. data/.github/workflows/rspec.yml +49 -0
  5. data/.github/workflows/rubocop.yml +20 -0
  6. data/.gitignore +1 -0
  7. data/.rubocop.yml +2 -2
  8. data/.rubocop_todo.yml +64 -35
  9. data/Appraisals +25 -15
  10. data/CHANGELOG.md +54 -0
  11. data/Gemfile +1 -0
  12. data/README.md +26 -1
  13. data/amoeba.gemspec +10 -12
  14. data/docs/contributing.md +19 -0
  15. data/docs/test_refactor.md +20 -0
  16. data/gemfiles/activerecord_6.1.gemfile +8 -0
  17. data/gemfiles/activerecord_7.0.gemfile +3 -0
  18. data/gemfiles/{activerecord_6.0.gemfile → activerecord_7.1.gemfile} +3 -1
  19. data/gemfiles/{activerecord_5.2.gemfile → activerecord_8.0.gemfile} +4 -2
  20. data/gemfiles/activerecord_8.1.gemfile +21 -0
  21. data/gemfiles/activerecord_head.gemfile +3 -1
  22. data/gemfiles/jruby_activerecord_7.0.gemfile +2 -2
  23. data/gemfiles/jruby_activerecord_head.gemfile +2 -0
  24. data/lib/amoeba/cloner.rb +4 -2
  25. data/lib/amoeba/config.rb +8 -3
  26. data/lib/amoeba/version.rb +1 -1
  27. data/spec/lib/amoeba_spec.rb +405 -225
  28. data/spec/spec_helper.rb +3 -1
  29. data/spec/support/data.rb +2 -0
  30. data/spec/support/models.rb +11 -2
  31. data/spec/support/schema.rb +2 -0
  32. metadata +26 -43
  33. data/.github/workflows/ruby_25.yml +0 -30
  34. data/.github/workflows/ruby_26.yml +0 -30
  35. data/.github/workflows/ruby_27.yml +0 -30
  36. data/.github/workflows/ruby_30.yml +0 -34
  37. data/.github/workflows/ruby_31.yml +0 -34
  38. data/.github/workflows/ruby_32.yml +0 -34
  39. data/.github/workflows/ruby_head.yml +0 -34
@@ -1,185 +1,238 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe 'amoeba' do
4
- context 'dup' do
5
- before do
6
- require ::File.dirname(__FILE__) + '/../support/data.rb'
6
+ context 'amoeba_dup' do
7
+ before { require ::File.dirname(__FILE__) + '/../support/data.rb' }
8
+
9
+ context 'with posts' do
10
+ subject(:save_post) { new_post.save! }
11
+
12
+ let(:old_post) { ::Post.find(1) }
13
+ let(:new_post) { old_post.amoeba_dup }
14
+
15
+ before do
16
+ old_post.class.amoeba do
17
+ prepend contents: "Here's a copy: "
18
+ end
19
+ end
20
+
21
+ # Unecessary. Included in refactor for completeness.
22
+ it { expect(old_post.comments.map(&:contents).include?('I love it!')).to be_truthy }
23
+
24
+ it { is_expected.to be_truthy }
25
+
26
+ it do
27
+ save_post
28
+ expect(new_post.title).to eq("Copy of #{old_post.title}")
29
+ end
30
+
31
+ it { expect { save_post }.not_to change(Tag.all, :count) }
32
+ it { expect { save_post }.not_to change(Category, :count) }
33
+
34
+ # The original versions of these tests counted checked that the counts of
35
+ # various models were multipled when the new instance is saved. This
36
+ # depends on there being no other data in the database, which is not
37
+ # reliable. The new versions of these tests check for an increase in the
38
+ # count by a certain amount. This depends on how the test data is created.
39
+
40
+ # Testing a 'has_one' relation with default amoeba configuration.
41
+ it { expect { save_post }.to change(Account, :count).by(1) }
42
+
43
+ # Testing a 'has_one ... through' relation with default amoeba
44
+ # configuration.
45
+ it { expect { save_post }.to change(History.all, :count).by(1) }
46
+
47
+ # Testing a 'has_many' relation with default amoeba configuration.
48
+ it { expect { save_post }.to change(Supercat.all, :count).by(3) }
49
+
50
+ # Testing that a new instance is created when a duplicate is saved.
51
+ it { expect { save_post }.to change(Post.all, :count).by(1) }
52
+
53
+ # Testing a 'has_one' relation with default amoeba configuration.
54
+ it { expect { save_post }.to change(PostConfig.all, :count).by(1) }
55
+
56
+ # The dup adds extra comments based on 'customize'. Three comments are
57
+ # added and two new ones are created.
58
+ it { expect { save_post }.to change(Comment.all, :count).by(5) }
59
+
60
+ # Ratings are linked to comments. The dup will duplicate the ratings for
61
+ # the existing comments (6 each for the test data) and the new comments
62
+ # will have no ratings.
63
+ it { expect { save_post }.to change(Rating.all, :count).by(18) }
64
+
65
+ # Testing a 'has_and_belongs_to_many' relationship with the default amoeba
66
+ # configuration.
67
+ # The `tag_count` method fetches the number of records in the join table
68
+ # and this tests that the three tags on the post are applied to the new
69
+ # post.
70
+ it { expect { save_post }.to change(Post, :tag_count).by(3) }
71
+
72
+ # Testing a 'has_one ... through' relationship with the 'clone' amoeba
73
+ # configuration.
74
+ # The three widgets that are linked, via a join table, should be duplicated
75
+ # in the Widget model and add corresponding records in the join table,
76
+ # PostWidget.
77
+ it { expect { save_post }.to change(Widget.all, :count).by(3) }
78
+ it { expect { save_post }.to change(PostWidget, :count).by(3) }
79
+
80
+ # Testing a 'has_and_belongs_to_many' relationship with the 'clone' amoeba
81
+ # configuration.
82
+ # There are three notes attached to the post so three new notes should be
83
+ # created in the Note model as well as three records in the join table. The
84
+ # `note_count` method fetches the number of records in the join table.
85
+ it { expect { save_post }.to change(Note.all, :count).by(3) }
86
+ it { expect { save_post }.to change(Post, :note_count).by(3) }
87
+
88
+ # Testing the 'include_association' amoeba configuration.
89
+ # Each Post can have many Supercats and each Supercat can have many
90
+ # Supperkitens. In the test data the post has three supercats and each
91
+ # supercat has three superkittens (how many were there going to St Ives?).
92
+ # As a result 9 extra Superkittens should be generated.
93
+ it { expect { save_post }.to change(Superkitten.all, :count).by(9) }
94
+
95
+ # Testing 'prepend' configuration on attribute.
96
+ # The attribute `ramblings` is configured to prepend 'Copy of '.
97
+ it do
98
+ expect(new_post.supercats.map(&:ramblings))
99
+ .to contain_exactly('Copy of zomg', 'Copy of why', 'Copy of ohnoes')
100
+ end
101
+
102
+ # Testing 'set' configuration on attribute.
103
+ # The attribute 'other_ramblings' is configured to set to 'La la la'.
104
+ # The original tests check that (a) the values are unique and (b) the value
105
+ # is set correctly. This new test will check that there are three values
106
+ # that are all set the same.
107
+ it { expect(new_post.supercats.map(&:other_ramblings)).to contain_exactly('La la la', 'La la la', 'La la la') }
108
+
109
+ # Testing various string modifications.
110
+ # * 'prepend' to add "Here's a copy: ", defined in the 'before' above
111
+ # * 'append' to add " (copied version)"
112
+ # * 'regext' to replace "dog" with "cat"
113
+ it do
114
+ expect(new_post.contents)
115
+ .to eq("Here's a copy: Lorum ipsum dolor rainbow bright. I like cats, cats are awesome. (copied version)")
116
+ end
117
+
118
+ # Testing 'customize' configuration.
119
+ # Two of the comments in the old post have 'nerf' values that cause lambdas
120
+ # to generate additional when the post is duplicated.
121
+ # The old post has three comments so the new post will have five comments.
122
+ it { expect(new_post.comments.length).to eq(5) }
123
+ # When the comment's 'nerf' value is 'ratatat' the new post will have the
124
+ # comment duplicated.
125
+ # The 'where' will only work once new_post has been saved.
126
+ it { expect(new_post.tap(&:save).comments.where(nerf: 'ratatat').length).to eq(2) }
127
+ # The duplicate will have a nil 'contents' while the other version is not.
128
+ it { expect(new_post.tap(&:save).comments.where(nerf: 'ratatat', contents: nil).length).to eq(1) }
129
+ # When the comment's 'nerf' value is 'bonk' the new post will have the
130
+ # comment duplicated. The new comment will have a modified 'nerf' value.
131
+ # The 'where' will only work once new_post has been saved.
132
+ # There will only be a single comment with the original 'nerf' value.
133
+ it { expect(new_post.tap(&:save).comments.where(nerf: 'bonk').length).to eq(1) }
134
+ # The duplicate will have a modified 'nerf' value.
135
+ it { expect(new_post.tap(&:save).comments.where(nerf: 'bonkers', contents: nil).length).to eq(1) }
136
+
137
+ # Testing duplicates with a 'has_many ... through' reference and a 'clone'
138
+ # amoeba configuration results in new records (with new ids).
139
+ it { expect(old_post.widgets.map(&:id) & new_post.tap(&:save).widgets.map(&:id)).to be_empty }
140
+
141
+ # Testing with a value that is serialized with a custom serializer.
142
+ # The 'value' of a CustomThing is a string that is serialized as an array.
143
+ # The test data has three instances of CustomThing; [], [1, 2] and [78]
144
+ it { expect(new_post.tap(&:save).custom_things.pluck(:value)).to contain_exactly([], [1, 2], [78]) }
7
145
  end
8
146
 
9
- let(:first_product) { Product.find(1) }
147
+ context 'with authors' do
148
+ let(:old_author) { Author.find(1) }
149
+ let(:new_author) { old_author.amoeba_dup.tap(&:save!) }
10
150
 
11
- it 'duplicates associated child records' do
12
- # Posts {{{
13
- old_post = ::Post.find(1)
14
- expect(old_post.comments.map(&:contents).include?('I love it!')).to be_truthy
151
+ it { expect(new_author.errors.messages).to be_empty }
15
152
 
16
- old_post.class.amoeba do
17
- prepend contents: "Here's a copy: "
153
+ # An author has multiple posts (default amoeba configuration) and posts
154
+ # in turn have multiple custom things (default amoeba configuration). The
155
+ # custom things have a string value field that is serialized via a custom
156
+ # serializer into an array.
157
+ # The test author has three posts, each of which have three custom
158
+ # things. These should be duplicated.
159
+ it { expect(new_author.posts.first.custom_things.map(&:value)).to contain_exactly([], [1, 2], [78]) }
160
+ end
161
+
162
+ context 'with inherited classes' do
163
+ # Testing the effect of single-table inheritance on amoeba duplication.
164
+ # The Product class has two subclasses, Shirt and Necklace. Each class
165
+ # has the same amoeba configuration that is defined with the 'raised'
166
+ # option.
167
+ # These tests are copied from the original and I do not fully understand
168
+ # the purpose of some of them as the behavior appears to be the same for
169
+ # all three classes.
170
+
171
+ subject(:new_product) { old_product.amoeba_dup.tap(&:save) }
172
+
173
+ context 'with the base class' do
174
+ let(:old_product) { Product.find(1) }
175
+ # The base product class has two relations;
176
+ # * image; has_many with default amoeba configuration
177
+ # * section; has_and_belongs_to_many with default amoeba configuration
178
+ # The amoeba configuration is defined with the propogate option.
179
+
180
+ # Duplicating product does not result in errors.
181
+ it { expect(new_product.errors.messages).to be_empty }
182
+
183
+ # New product copies all images from old product. The old product has
184
+ # two images so two new images are created for the new product.
185
+ it { expect { new_product }.to change(Image, :count).by(2) }
186
+ it { expect { new_product }.not_to change(old_product.images, :count) }
187
+ it { expect(new_product.images.count).to eq(old_product.images.count) }
188
+
189
+ # New product copies references to all sections from old product. No
190
+ # new sections are created and the new product is linked to the same
191
+ # number of sections as the old product.
192
+ it { expect { new_product }.not_to change(Section.all, :count) }
193
+ it { expect { new_product }.not_to change(old_product, :section_count) }
194
+ it { expect(new_product.section_count).to eq(old_product.section_count) }
18
195
  end
19
196
 
20
- new_post = old_post.amoeba_dup
21
-
22
- start_account_count = Account.all.count
23
- start_history_count = History.all.count
24
- start_cat_count = Category.all.count
25
- start_supercat_count = Supercat.all.count
26
- start_tag_count = Tag.all.count
27
- start_note_count = Note.all.count
28
- start_widget_count = Widget.all.count
29
- start_post_count = Post.all.count
30
- start_comment_count = Comment.all.count
31
- start_rating_count = Rating.all.count
32
- start_postconfig_count = PostConfig.all.count
33
- start_postwidget_count = PostWidget.all.count
34
- start_superkitten_count = Superkitten.all.count
35
- start_posttag_count = Post.tag_count
36
- start_postnote_count = Post.note_count
37
-
38
- expect(new_post.save!).to be_truthy
39
- expect(new_post.title).to eq("Copy of #{old_post.title}")
40
-
41
- end_account_count = Account.count
42
- end_history_count = History.count
43
- end_cat_count = Category.count
44
- end_supercat_count = Supercat.count
45
- end_tag_count = Tag.all.count
46
- end_note_count = Note.all.count
47
- end_widget_count = Widget.all.count
48
- end_post_count = Post.all.count
49
- end_comment_count = Comment.all.count
50
- end_rating_count = Rating.all.count
51
- end_postconfig_count = PostConfig.all.count
52
- end_postwidget_count = PostWidget.all.count
53
- end_superkitten_count = Superkitten.all.count
54
- end_posttag_count = Post.tag_count
55
- end_postnote_count = Post.note_count
56
-
57
- expect(end_tag_count).to eq(start_tag_count)
58
- expect(end_cat_count).to eq(start_cat_count)
59
- expect(end_account_count).to eq(start_account_count * 2)
60
- expect(end_history_count).to eq(start_history_count * 2)
61
- expect(end_supercat_count).to eq(start_supercat_count * 2)
62
- expect(end_post_count).to eq(start_post_count * 2)
63
- expect(end_comment_count).to eq((start_comment_count * 2) + 2)
64
- expect(end_rating_count).to eq(start_rating_count * 2)
65
- expect(end_postconfig_count).to eq(start_postconfig_count * 2)
66
- expect(end_posttag_count).to eq(start_posttag_count * 2)
67
- expect(end_widget_count).to eq(start_widget_count * 2)
68
- expect(end_postwidget_count).to eq(start_postwidget_count * 2)
69
- expect(end_note_count).to eq(start_note_count * 2)
70
- expect(end_postnote_count).to eq(start_postnote_count * 2)
71
- expect(end_superkitten_count).to eq(start_superkitten_count * 2)
72
-
73
- expect(new_post.supercats.map(&:ramblings)).to include('Copy of zomg')
74
- expect(new_post.supercats.map(&:other_ramblings).uniq.length).to eq(1)
75
- expect(new_post.supercats.map(&:other_ramblings).uniq).to include('La la la')
76
- expect(new_post.contents).to eq("Here's a copy: #{old_post.contents.gsub(/dog/,
77
- 'cat')} (copied version)")
78
- expect(new_post.comments.length).to eq(5)
79
- expect(new_post.comments.select do |c|
80
- c.nerf == 'ratatat' && c.contents.nil?
81
- end.length).to eq(1)
82
- expect(new_post.comments.select { |c| c.nerf == 'ratatat' }.length).to eq(2)
83
- expect(new_post.comments.select { |c| c.nerf == 'bonk' }.length).to eq(1)
84
- expect(new_post.comments.select do |c|
85
- c.nerf == 'bonkers' && c.contents.nil?
86
- end.length).to eq(1)
87
-
88
- new_post.widgets.map(&:id).each do |id|
89
- expect(old_post.widgets.map(&:id)).not_to include(id)
197
+ context "with a class inheriting with the 'raised :submissive' configuration" do
198
+ let(:old_product) { Shirt.find(2) }
199
+
200
+ # Duplicating product does not result in errors.
201
+ it { expect(new_product.errors.messages).to be_empty }
202
+
203
+ # New product copies all images from old product. The old product has
204
+ # two images so two new images are created for the new product.
205
+ it { expect { new_product }.to change(Image, :count).by(2) }
206
+ it { expect { new_product }.not_to change(old_product.images, :count) }
207
+ it { expect(new_product.images.count).to eq(old_product.images.count) }
208
+
209
+ # New product copies references to all sections from old product. No
210
+ # new sections are created and the new product is linked to the same
211
+ # number of sections as the old product.
212
+ it { expect { new_product }.not_to change(Section.all, :count) }
213
+ it { expect { new_product }.not_to change(old_product, :section_count) }
214
+ it { expect(new_product.section_count).to eq(old_product.section_count) }
90
215
  end
91
216
 
92
- expect(new_post.custom_things.length).to eq(3)
93
- expect(new_post.custom_things.select { |ct| ct.value == [] }.length).to eq(1)
94
- expect(new_post.custom_things.select { |ct| ct.value == [1, 2] }.length).to eq(1)
95
- expect(new_post.custom_things.select { |ct| ct.value == [78] }.length).to eq(1)
96
- # }}}
97
- # Author {{{
98
- old_author = Author.find(1)
99
- new_author = old_author.amoeba_dup
100
- new_author.save!
101
- expect(new_author.errors.messages).to be_empty
102
- expect(new_author.posts.first.custom_things.length).to eq(3)
103
- expect(new_author.posts.first.custom_things.select { |ct| ct.value == [] }.length).to eq(1)
104
- expect(new_author.posts.first.custom_things.select do |ct|
105
- ct.value == [1, 2]
106
- end.length).to eq(1)
107
- expect(new_author.posts.first.custom_things.select { |ct| ct.value == [78] }.length).to eq(1)
108
- # }}}
109
- # Products {{{
110
- # Base Class {{{
111
-
112
- start_image_count = first_product.images.count
113
- start_section_count = Section.all.length
114
- start_prodsection_count = first_product.section_count
115
-
116
- new_product = first_product.amoeba_dup
117
- new_product.save
118
- expect(new_product.errors.messages).to be_empty
119
-
120
- end_image_count = first_product.images.count
121
- end_newimage_count = new_product.images.count
122
- end_section_count = Section.all.length
123
- end_prodsection_count = first_product.section_count
124
- end_newprodsection_count = new_product.section_count
125
-
126
- expect(end_image_count).to eq(start_image_count)
127
- expect(end_newimage_count).to eq(start_image_count)
128
- expect(end_section_count).to eq(start_section_count)
129
- expect(end_prodsection_count).to eq(start_prodsection_count)
130
- expect(end_newprodsection_count).to eq(start_prodsection_count)
131
- # }}}
132
-
133
- # Inherited Class {{{
134
- # Shirt {{{
135
- old_product = Shirt.find(2)
136
-
137
- start_image_count = old_product.images.count
138
- start_section_count = Section.count
139
- start_prodsection_count = old_product.section_count
140
-
141
- new_product = old_product.amoeba_dup
142
- new_product.save
143
- expect(new_product.errors.messages).to be_empty
144
-
145
- end_image_count = old_product.images.count
146
- end_newimage_count = new_product.images.count
147
- end_section_count = Section.count
148
- end_prodsection_count = first_product.section_count
149
- end_newprodsection_count = new_product.section_count
150
-
151
- expect(end_image_count).to eq(start_image_count)
152
- expect(end_newimage_count).to eq(start_image_count)
153
- expect(end_section_count).to eq(start_section_count)
154
- expect(end_prodsection_count).to eq(start_prodsection_count)
155
- expect(end_newprodsection_count).to eq(start_prodsection_count)
156
- # }}}
157
-
158
- # Necklace {{{
159
- old_product = Necklace.find(3)
160
-
161
- start_image_count = old_product.images.count
162
- start_section_count = Section.count
163
- start_prodsection_count = old_product.section_count
164
-
165
- new_product = old_product.amoeba_dup
166
- new_product.save
167
- expect(new_product.errors.messages).to be_empty
168
-
169
- end_image_count = old_product.images.count
170
- end_newimage_count = new_product.images.count
171
- end_section_count = Section.count
172
- end_prodsection_count = first_product.section_count
173
- end_newprodsection_count = new_product.section_count
174
-
175
- expect(end_image_count).to eq(start_image_count)
176
- expect(end_newimage_count).to eq(start_image_count)
177
- expect(end_section_count).to eq(start_section_count)
178
- expect(end_prodsection_count).to eq(start_prodsection_count)
179
- expect(end_newprodsection_count).to eq(start_prodsection_count)
180
- # }}}
181
- # }}}
182
- # }}}
217
+ context "with a class inheriting with the 'raised :relaxed' configuration" do
218
+ let(:old_product) { Necklace.find(3) }
219
+
220
+ # Duplicating product does not result in errors.
221
+ it { expect(new_product.errors.messages).to be_empty }
222
+
223
+ # New product copies all images from old product. The old product has
224
+ # two images so two new images are created for the new product.
225
+ it { expect { new_product }.to change(Image, :count).by(2) }
226
+ it { expect { new_product }.not_to change(old_product.images, :count) }
227
+ it { expect(new_product.images.count).to eq(old_product.images.count) }
228
+
229
+ # New product copies references to all sections from old product. No
230
+ # new sections are created and the new product is linked to the same
231
+ # number of sections as the old product.
232
+ it { expect { new_product }.not_to change(Section.all, :count) }
233
+ it { expect { new_product }.not_to change(old_product, :section_count) }
234
+ it { expect(new_product.section_count).to eq(old_product.section_count) }
235
+ end
183
236
  end
184
237
  end
185
238
 
@@ -251,50 +304,131 @@ describe 'amoeba' do
251
304
  end
252
305
  end
253
306
 
254
- context 'override' do
307
+ context 'when there is an override configured' do
308
+ subject(:image_dup) { image.amoeba_dup }
309
+
310
+ let(:image) { Image.create(filename: 'test.jpg', product_id: 12) }
311
+
255
312
  before do
256
- ::Image.fresh_amoeba
257
- ::Image.amoeba do
313
+ Image.fresh_amoeba
314
+ Image.amoeba do
258
315
  override ->(old, new) { new.product_id = 13 if old.filename == 'test.jpg' }
259
316
  end
260
317
  end
261
318
 
262
- it 'overrides fields' do
263
- image = ::Image.create(filename: 'test.jpg', product_id: 12)
264
- image_dup = image.amoeba_dup
265
- expect(image_dup.save).to be_truthy
266
- expect(image_dup.product_id).to eq(13)
267
- end
268
-
269
- it 'does not override fields' do
270
- image = ::Image.create(filename: 'test2.jpg', product_id: 12)
271
- image_dup = image.amoeba_dup
272
- expect(image_dup.save).to be_truthy
273
- expect(image_dup.product_id).to eq(12)
274
- end
319
+ it { is_expected.to be_truthy }
320
+ it { expect(image_dup.product_id).to eq(13) }
275
321
  end
276
322
 
277
- context 'nullify' do
323
+ context 'when there is an override configured that does not apply' do
324
+ subject(:image_dup) { image.amoeba_dup }
325
+
326
+ let(:image) { Image.create(filename: 'test2.jpg', product_id: 12) }
327
+
278
328
  before do
279
- ::Image.fresh_amoeba
280
- ::Image.amoeba do
281
- nullify :product_id
329
+ Image.fresh_amoeba
330
+ Image.amoeba do
331
+ override ->(old, new) { new.product_id = 13 if old.filename == 'test.jpg' }
282
332
  end
283
333
  end
284
334
 
335
+ it { is_expected.to be_truthy }
336
+ it { expect(image_dup.product_id).to eq(12) }
337
+ end
338
+
339
+ context 'with a field configured with nullify' do
340
+ subject(:image_dup) { image.amoeba_dup }
341
+
285
342
  let(:image) { ::Image.create(filename: 'test.jpg', product_id: 12) }
286
- let(:image_dup) { image.amoeba_dup }
287
343
 
288
- it 'nullifies fields' do
289
- expect(image_dup.save).to be_truthy
290
- expect(image_dup.product_id).to be_nil
344
+ before { ::Image.fresh_amoeba }
345
+
346
+ context 'without a condition' do
347
+ before do
348
+ ::Image.amoeba do
349
+ nullify :product_id
350
+ end
351
+ end
352
+
353
+ it { is_expected.to be_truthy }
354
+ it { expect(image_dup.product_id).to be_nil }
355
+
356
+ it 'stores the field with no options' do
357
+ expect(::Image.amoeba.null_fields).to eq(product_id: {})
358
+ end
359
+ end
360
+
361
+ context 'with a truthy if condition' do
362
+ before do
363
+ ::Image.amoeba do
364
+ nullify :product_id, if: :truthy?
365
+ end
366
+ end
367
+
368
+ it { expect(image_dup.product_id).to be_nil }
369
+ end
370
+
371
+ context 'with a falsey if condition' do
372
+ before do
373
+ ::Image.amoeba do
374
+ nullify :product_id, if: :falsey?
375
+ end
376
+ end
377
+
378
+ it { expect(image_dup.product_id).to eq(12) }
379
+
380
+ it 'stores the condition alongside the field' do
381
+ expect(::Image.amoeba.null_fields).to eq(product_id: { if: :falsey? })
382
+ end
383
+ end
384
+
385
+ context 'with an array of fields and a falsey if condition' do
386
+ before do
387
+ ::Image.amoeba do
388
+ nullify %i[filename product_id], if: :falsey?
389
+ end
390
+ end
391
+
392
+ it { expect(image_dup.filename).to eq('test.jpg') }
393
+ it { expect(image_dup.product_id).to eq(12) }
394
+ end
395
+
396
+ context 'when an array of fields replaces the previously configured ones' do
397
+ before do
398
+ ::Image.amoeba do
399
+ nullify :filename
400
+ nullify [:product_id]
401
+ end
402
+ end
403
+
404
+ it { expect(image_dup.filename).to eq('test.jpg') }
405
+ it { expect(image_dup.product_id).to be_nil }
406
+
407
+ it 'drops the replaced field from null_fields' do
408
+ expect(::Image.amoeba.null_fields).to eq(product_id: {})
409
+ end
410
+ end
411
+
412
+ context 'when a field is redeclared without a condition' do
413
+ before do
414
+ ::Image.amoeba do
415
+ nullify :product_id, if: :falsey?
416
+ nullify :product_id
417
+ end
418
+ end
419
+
420
+ it { expect(image_dup.product_id).to be_nil }
421
+
422
+ it 'clears the previous condition' do
423
+ expect(::Image.amoeba.null_fields).to eq(product_id: {})
424
+ end
291
425
  end
292
426
  end
293
427
 
294
428
  context 'strict propagate' do
295
429
  it 'calls #reset_amoeba' do
296
- expect(::SuperBlackBox).to receive(:reset_amoeba).and_call_original
297
- box = ::SuperBlackBox.create(title: 'Super Black Box', price: 9.99, length: 1, metal: '1')
430
+ allow(SuperBlackBox).to receive(:reset_amoeba).and_call_original
431
+ box = SuperBlackBox.create(title: 'Super Black Box', price: 9.99, length: 1, metal: '1')
298
432
  new_box = box.amoeba_dup
299
433
  expect(new_box.save).to be_truthy
300
434
  end
@@ -335,46 +469,92 @@ describe 'amoeba' do
335
469
  end
336
470
  end
337
471
 
338
- context 'inheritance' do
472
+ context 'with inheritance' do
473
+ # Box
474
+ # has_many :products, class_name: 'BoxProduct'
475
+ # has_many :sub_products, class_name: 'BoxSubProduct'
476
+ #
477
+ # BoxProduct
478
+ # belongs_to :box, class_name: 'Box'
479
+ # amoeba do
480
+ # enable
481
+ # propagate
482
+ # end
483
+ #
484
+ # BoxSubProduct < BoxProduct
485
+ # has_one :another_product, class_name: 'BoxAnotherProduct'
486
+ #
487
+ # BoxSubSubProduct < BoxSubProduct
488
+ #
489
+ # BoxAnotherProduct < BoxProduct
490
+ # belongs_to :sub_product, class_name: 'BoxSubProduct'
491
+ #
492
+ # This test, from the original suite, appears to be testing
493
+ # * BoxProduct has amoeba configured with 'propagate'
494
+ # * BoxSubProduct inherits (STI) from BoxProduct
495
+ # * BoxSubSubProduct inherits (STI) from BoxSubProduct
496
+ #
497
+ # When the instance of BoxSubProduct is duplicated as a result of an
498
+ # an instance of Box being duplicated then amoeba also acts on it to make
499
+ # a duplicate of the instance of `another_product`.
500
+ #
501
+ # This test could be improved to also test the behaviour when `propagate`
502
+ # is not used.
503
+
339
504
  let(:box) { Box.create }
505
+ let(:sub_sub_product) { BoxSubSubProduct.create(title: 'Awesome shoes') }
506
+ let(:another_product) { BoxAnotherProduct.create(title: 'Cleaning product') }
340
507
 
341
- it 'does not fail with a deep inheritance' do
342
- sub_sub_product = BoxSubSubProduct.create(title: 'Awesome shoes')
343
- another_product = BoxAnotherProduct.create(title: 'Cleaning product')
344
- sub_sub_product.update(box: box, another_product: another_product)
345
- expect(box.sub_products.first.another_product.title).to eq('Cleaning product')
346
- expect(box.amoeba_dup.sub_products.first.another_product.title).to eq('Cleaning product')
347
- end
508
+ before { sub_sub_product.update(box: box, another_product: another_product) }
509
+
510
+ it { expect(box.amoeba_dup.sub_products.first.another_product.title).to eq('Cleaning product') }
348
511
  end
349
512
 
350
- context 'inheritance extended' do
351
- subject { stage.amoeba_dup }
513
+ context 'with inheritance extended' do
514
+ subject(:stage_dup) { stage.amoeba_dup }
352
515
 
353
- let(:stage) do
354
- stage = CustomStage.new(title: 'My Stage', external_id: 213)
516
+ # CustomStage inherits from Stage
517
+ # Stage has amoeba configured with `propagate`
518
+ # These tests are not exactly equivalent to the previous verison. Formerly
519
+ # all the expectations were in a single block and `save!` was executed on
520
+ # the subject. This does not need to be done in some cases.
521
+
522
+ let(:stage) { CustomStage.new(title: 'My Stage', external_id: 213) }
523
+
524
+ before do
355
525
  stage.listeners.build(name: 'John')
356
526
  stage.listeners.build(name: 'Helen')
357
527
  stage.specialists.build(name: 'Jack')
358
528
  stage.custom_rules.build(description: 'Kill all humans')
359
529
  stage.save!
360
- stage
361
530
  end
362
531
 
363
- it 'contains parent association and own associations', :aggregate_failures do
364
- subject
365
- expect { subject.save! }.to change(Listener, :count).by(2)
366
- .and change(Specialist, :count).by(1)
367
- .and change(
368
- CustomRule, :count
369
- ).by(1)
370
-
371
- expect(subject.title).to eq 'My Stage'
372
- expect(subject.external_id).to be_nil
373
- expect(subject.listeners.find_by(name: 'John')).not_to be_nil
374
- expect(subject.listeners.find_by(name: 'Helen')).not_to be_nil
375
- expect(subject.specialists.find_by(name: 'Jack')).not_to be_nil
376
- expect(subject.custom_rules.first.description).to eq 'Kill all humans'
377
- end
532
+ # Stage has the listeners and specialist has_many attributes configured
533
+ # with amoeba with `include_association`. This is inherited by CustomStage.
534
+ it { expect { stage_dup.save! }.to change(Listener, :count).by(2) }
535
+ it { expect(stage_dup.tap(&:save!).listeners.find_by(name: 'John')).not_to be_nil }
536
+ it { expect(stage_dup.tap(&:save!).listeners.find_by(name: 'Helen')).not_to be_nil }
537
+ it { expect { stage_dup.save! }.to change(Specialist, :count).by(1) }
538
+ it { expect(stage_dup.tap(&:save!).specialists.find_by(name: 'Jack')).not_to be_nil }
539
+
540
+ # CustomStage has the custom_rules has_many attribute configured with
541
+ # amoeba with `include_association`. This attribute does not exist for
542
+ # Stage.
543
+ it { expect { stage_dup.save! }.to change(CustomRule, :count).by(1) }
544
+ # save! is not required here as it is testing the unsaved instance of
545
+ # CustomRule. This differs from the tests above for listeners and
546
+ # specialists which uses find_by. This is testing the same thing but needs
547
+ # the instances to be in the database.
548
+ it { expect(stage_dup.custom_rules.first.description).to eq 'Kill all humans' }
549
+
550
+ # Stage has an attribute title that is not explicitly configured for amoeba
551
+ # and so has the default configuration. CustomStage inherits this
552
+ # configuration.
553
+ it { expect(stage_dup.title).to eq 'My Stage' }
554
+
555
+ # Stage has an attribute external_id that is configured for amoeba with
556
+ # nullify. CustomStage inherits this configuration.
557
+ it { expect(stage_dup.external_id).to be_nil }
378
558
  end
379
559
 
380
560
  context 'polymorphic' do