amoeba 3.2.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/activerecord_head.yml +26 -0
  3. data/.github/workflows/jruby.yml +30 -0
  4. data/.github/workflows/rspec.yml +49 -0
  5. data/.github/workflows/rubocop.yml +20 -0
  6. data/.gitignore +2 -0
  7. data/.rubocop.yml +12 -8
  8. data/.rubocop_todo.yml +149 -0
  9. data/Appraisals +31 -38
  10. data/CHANGELOG.md +68 -0
  11. data/Gemfile +6 -2
  12. data/LICENSE.md +11 -0
  13. data/README.md +28 -17
  14. data/Rakefile +2 -0
  15. data/amoeba.gemspec +18 -17
  16. data/docs/contributing.md +19 -0
  17. data/docs/test_refactor.md +20 -0
  18. data/gemfiles/activerecord_6.1.gemfile +17 -8
  19. data/gemfiles/activerecord_7.0.gemfile +22 -0
  20. data/gemfiles/activerecord_7.1.gemfile +21 -0
  21. data/gemfiles/activerecord_8.0.gemfile +21 -0
  22. data/gemfiles/activerecord_8.1.gemfile +21 -0
  23. data/gemfiles/activerecord_head.gemfile +12 -13
  24. data/gemfiles/jruby_activerecord_7.0.gemfile +20 -0
  25. data/gemfiles/jruby_activerecord_head.gemfile +15 -15
  26. data/lib/amoeba/class_methods.rb +2 -0
  27. data/lib/amoeba/cloner.rb +15 -6
  28. data/lib/amoeba/config.rb +30 -53
  29. data/lib/amoeba/instance_methods.rb +3 -0
  30. data/lib/amoeba/macros/base.rb +4 -1
  31. data/lib/amoeba/macros/has_and_belongs_to_many.rb +2 -0
  32. data/lib/amoeba/macros/has_many.rb +2 -0
  33. data/lib/amoeba/macros/has_one.rb +4 -0
  34. data/lib/amoeba/macros.rb +2 -0
  35. data/lib/amoeba/version.rb +3 -1
  36. data/lib/amoeba.rb +6 -2
  37. data/spec/lib/amoeba_spec.rb +425 -231
  38. data/spec/spec_helper.rb +16 -6
  39. data/spec/support/data.rb +23 -14
  40. data/spec/support/models.rb +50 -40
  41. data/spec/support/schema.rb +2 -0
  42. metadata +53 -39
  43. data/.travis.yml +0 -110
  44. data/gemfiles/activerecord_4.2.gemfile +0 -18
  45. data/gemfiles/activerecord_5.0.gemfile +0 -18
  46. data/gemfiles/activerecord_5.1.gemfile +0 -18
  47. data/gemfiles/activerecord_5.2.gemfile +0 -18
  48. data/gemfiles/activerecord_6.0.gemfile +0 -18
  49. data/gemfiles/jruby_activerecord_6.1.gemfile +0 -19
@@ -1,291 +1,434 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe 'amoeba' do
4
- context 'dup' do
5
- before :each 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/, 'cat')} (copied version)")
77
- expect(new_post.comments.length).to eq(5)
78
- expect(new_post.comments.select { |c| c.nerf == 'ratatat' && c.contents.nil? }.length).to eq(1)
79
- expect(new_post.comments.select { |c| c.nerf == 'ratatat' }.length).to eq(2)
80
- expect(new_post.comments.select { |c| c.nerf == 'bonk' }.length).to eq(1)
81
- expect(new_post.comments.select { |c| c.nerf == 'bonkers' && c.contents.nil? }.length).to eq(1)
82
-
83
- new_post.widgets.map(&:id).each do |id|
84
- 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) }
85
215
  end
86
216
 
87
- expect(new_post.custom_things.length).to eq(3)
88
- expect(new_post.custom_things.select { |ct| ct.value == [] }.length).to eq(1)
89
- expect(new_post.custom_things.select { |ct| ct.value == [1, 2] }.length).to eq(1)
90
- expect(new_post.custom_things.select { |ct| ct.value == [78] }.length).to eq(1)
91
- # }}}
92
- # Author {{{
93
- old_author = Author.find(1)
94
- new_author = old_author.amoeba_dup
95
- new_author.save!
96
- expect(new_author.errors.messages).to be_empty
97
- expect(new_author.posts.first.custom_things.length).to eq(3)
98
- expect(new_author.posts.first.custom_things.select { |ct| ct.value == [] }.length).to eq(1)
99
- expect(new_author.posts.first.custom_things.select { |ct| ct.value == [1, 2] }.length).to eq(1)
100
- expect(new_author.posts.first.custom_things.select { |ct| ct.value == [78] }.length).to eq(1)
101
- # }}}
102
- # Products {{{
103
- # Base Class {{{
104
-
105
- start_image_count = first_product.images.count
106
- start_section_count = Section.all.length
107
- start_prodsection_count = first_product.section_count
108
-
109
- new_product = first_product.amoeba_dup
110
- new_product.save
111
- expect(new_product.errors.messages).to be_empty
112
-
113
- end_image_count = first_product.images.count
114
- end_newimage_count = new_product.images.count
115
- end_section_count = Section.all.length
116
- end_prodsection_count = first_product.section_count
117
- end_newprodsection_count = new_product.section_count
118
-
119
- expect(end_image_count).to eq(start_image_count)
120
- expect(end_newimage_count).to eq(start_image_count)
121
- expect(end_section_count).to eq(start_section_count)
122
- expect(end_prodsection_count).to eq(start_prodsection_count)
123
- expect(end_newprodsection_count).to eq(start_prodsection_count)
124
- # }}}
125
-
126
- # Inherited Class {{{
127
- # Shirt {{{
128
- old_product = Shirt.find(2)
129
-
130
- start_image_count = old_product.images.count
131
- start_section_count = Section.count
132
- start_prodsection_count = old_product.section_count
133
-
134
- new_product = old_product.amoeba_dup
135
- new_product.save
136
- expect(new_product.errors.messages).to be_empty
137
-
138
- end_image_count = old_product.images.count
139
- end_newimage_count = new_product.images.count
140
- end_section_count = Section.count
141
- end_prodsection_count = first_product.section_count
142
- end_newprodsection_count = new_product.section_count
143
-
144
- expect(end_image_count).to eq(start_image_count)
145
- expect(end_newimage_count).to eq(start_image_count)
146
- expect(end_section_count).to eq(start_section_count)
147
- expect(end_prodsection_count).to eq(start_prodsection_count)
148
- expect(end_newprodsection_count).to eq(start_prodsection_count)
149
- # }}}
150
-
151
- # Necklace {{{
152
- old_product = Necklace.find(3)
153
-
154
- start_image_count = old_product.images.count
155
- start_section_count = Section.count
156
- start_prodsection_count = old_product.section_count
157
-
158
- new_product = old_product.amoeba_dup
159
- new_product.save
160
- expect(new_product.errors.messages).to be_empty
161
-
162
- end_image_count = old_product.images.count
163
- end_newimage_count = new_product.images.count
164
- end_section_count = Section.count
165
- end_prodsection_count = first_product.section_count
166
- end_newprodsection_count = new_product.section_count
167
-
168
- expect(end_image_count).to eq(start_image_count)
169
- expect(end_newimage_count).to eq(start_image_count)
170
- expect(end_section_count).to eq(start_section_count)
171
- expect(end_prodsection_count).to eq(start_prodsection_count)
172
- expect(end_newprodsection_count).to eq(start_prodsection_count)
173
- # }}}
174
- # }}}
175
- # }}}
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
176
236
  end
177
237
  end
178
238
 
179
239
  context 'Using a if condition' do
240
+ subject { post.amoeba_dup.save! }
241
+
180
242
  before(:all) do
181
243
  require ::File.dirname(__FILE__) + '/../support/data.rb'
182
244
  end
245
+
183
246
  before { ::Post.fresh_amoeba }
184
247
 
185
- subject { post.amoeba_dup.save! }
186
248
  let(:post) { Post.first }
187
249
 
188
250
  it 'includes an association with truthy condition' do
189
251
  ::Post.amoeba do
190
252
  include_association :comments, if: :truthy?
191
253
  end
192
- expect { subject }.to change { Comment.count }.by(3)
254
+ expect { subject }.to change(Comment, :count).by(3)
193
255
  end
194
256
 
195
257
  it 'does not include an association with a falsey condition' do
196
258
  ::Post.amoeba do
197
259
  include_association :comments, if: :falsey?
198
260
  end
199
- expect { subject }.not_to change { Comment.count }
261
+ expect { subject }.not_to change(Comment, :count)
200
262
  end
201
263
 
202
264
  it 'excludes an association with a truthy condition' do
203
265
  ::Post.amoeba do
204
266
  exclude_association :comments, if: :truthy?
205
267
  end
206
- expect { subject }.not_to change { Comment.count }
268
+ expect { subject }.not_to change(Comment, :count)
207
269
  end
208
270
 
209
271
  it 'does not exclude an association with a falsey condition' do
210
272
  ::Post.amoeba do
211
273
  exclude_association :comments, if: :falsey?
212
274
  end
213
- expect { subject }.to change { Comment.count }.by(3)
275
+ expect { subject }.to change(Comment, :count).by(3)
214
276
  end
215
277
 
216
278
  it 'includes associations from a given array with a truthy condition' do
217
279
  ::Post.amoeba do
218
280
  include_association [:comments], if: :truthy?
219
281
  end
220
- expect { subject }.to change { Comment.count }.by(3)
282
+ expect { subject }.to change(Comment, :count).by(3)
221
283
  end
222
284
 
223
285
  it 'does not include associations from a given array with a falsey condition' do
224
286
  ::Post.amoeba do
225
287
  include_association [:comments], if: :falsey?
226
288
  end
227
- expect { subject }.not_to change { Comment.count }
289
+ expect { subject }.not_to change(Comment, :count)
228
290
  end
229
291
 
230
292
  it 'does exclude associations from a given array with a truthy condition' do
231
293
  ::Post.amoeba do
232
294
  exclude_association [:comments], if: :truthy?
233
295
  end
234
- expect { subject }.not_to change { Comment.count }
296
+ expect { subject }.not_to change(Comment, :count)
235
297
  end
236
298
 
237
299
  it 'does not exclude associations from a given array with a falsey condition' do
238
300
  ::Post.amoeba do
239
301
  exclude_association [:comments], if: :falsey?
240
302
  end
241
- expect { subject }.to change { Comment.count }.by(3)
303
+ expect { subject }.to change(Comment, :count).by(3)
242
304
  end
243
305
  end
244
306
 
245
- context 'override' do
246
- before :each do
247
- ::Image.fresh_amoeba
248
- ::Image.amoeba 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
+
312
+ before do
313
+ Image.fresh_amoeba
314
+ Image.amoeba do
249
315
  override ->(old, new) { new.product_id = 13 if old.filename == 'test.jpg' }
250
316
  end
251
317
  end
252
318
 
253
- it 'should override fields' do
254
- image = ::Image.create(filename: 'test.jpg', product_id: 12)
255
- image_dup = image.amoeba_dup
256
- expect(image_dup.save).to be_truthy
257
- expect(image_dup.product_id).to eq(13)
258
- end
319
+ it { is_expected.to be_truthy }
320
+ it { expect(image_dup.product_id).to eq(13) }
321
+ end
322
+
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) }
259
327
 
260
- it 'should not override fields' do
261
- image = ::Image.create(filename: 'test2.jpg', product_id: 12)
262
- image_dup = image.amoeba_dup
263
- expect(image_dup.save).to be_truthy
264
- expect(image_dup.product_id).to eq(12)
328
+ before do
329
+ Image.fresh_amoeba
330
+ Image.amoeba do
331
+ override ->(old, new) { new.product_id = 13 if old.filename == 'test.jpg' }
332
+ end
265
333
  end
334
+
335
+ it { is_expected.to be_truthy }
336
+ it { expect(image_dup.product_id).to eq(12) }
266
337
  end
267
338
 
268
- context 'nullify' do
269
- before :each do
270
- ::Image.fresh_amoeba
271
- ::Image.amoeba do
272
- nullify :product_id
339
+ context 'with a field configured with nullify' do
340
+ subject(:image_dup) { image.amoeba_dup }
341
+
342
+ let(:image) { ::Image.create(filename: 'test.jpg', product_id: 12) }
343
+
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: {})
273
358
  end
274
359
  end
275
360
 
276
- let(:image) { ::Image.create(filename: 'test.jpg', product_id: 12) }
277
- let(:image_dup) { image.amoeba_dup }
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
278
367
 
279
- it 'should nullify fields' do
280
- expect(image_dup.save).to be_truthy
281
- expect(image_dup.product_id).to be_nil
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
282
425
  end
283
426
  end
284
427
 
285
428
  context 'strict propagate' do
286
- it 'should call #reset_amoeba' do
287
- expect(::SuperBlackBox).to receive(:reset_amoeba).and_call_original
288
- box = ::SuperBlackBox.create(title: 'Super Black Box', price: 9.99, length: 1, metal: '1')
429
+ it 'calls #reset_amoeba' do
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')
289
432
  new_box = box.amoeba_dup
290
433
  expect(new_box.save).to be_truthy
291
434
  end
@@ -312,55 +455,106 @@ describe 'amoeba' do
312
455
 
313
456
  context 'preprocessing fields' do
314
457
  subject { super_admin.amoeba_dup }
315
- let(:super_admin) { ::SuperAdmin.create!(email: 'user@example.com', active: true, password: 'password') }
316
458
 
317
- it 'should accept "set" to set false to attribute' do
459
+ let(:super_admin) do
460
+ ::SuperAdmin.create!(email: 'user@example.com', active: true, password: 'password')
461
+ end
462
+
463
+ it 'accepts "set" to set false to attribute' do
318
464
  expect(subject.active).to be false
319
465
  end
320
466
 
321
- it 'should skip "prepend" if it equal to false' do
467
+ it 'skips "prepend" if it equal to false' do
322
468
  expect(subject.password).to eq('password')
323
469
  end
324
470
  end
325
471
 
326
- 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
+
327
504
  let(:box) { Box.create }
505
+ let(:sub_sub_product) { BoxSubSubProduct.create(title: 'Awesome shoes') }
506
+ let(:another_product) { BoxAnotherProduct.create(title: 'Cleaning product') }
328
507
 
329
- it 'does not fail with a deep inheritance' do
330
- sub_sub_product = BoxSubSubProduct.create(title: 'Awesome shoes')
331
- another_product = BoxAnotherProduct.create(title: 'Cleaning product')
332
- sub_sub_product.update(box: box, another_product: another_product)
333
- expect(box.sub_products.first.another_product.title).to eq('Cleaning product')
334
- expect(box.amoeba_dup.sub_products.first.another_product.title).to eq('Cleaning product')
335
- 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') }
336
511
  end
337
512
 
338
- context 'inheritance extended' do
339
- let(:stage) do
340
- stage = CustomStage.new(title: 'My Stage', external_id: 213)
513
+ context 'with inheritance extended' do
514
+ subject(:stage_dup) { stage.amoeba_dup }
515
+
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
341
525
  stage.listeners.build(name: 'John')
342
526
  stage.listeners.build(name: 'Helen')
343
527
  stage.specialists.build(name: 'Jack')
344
528
  stage.custom_rules.build(description: 'Kill all humans')
345
529
  stage.save!
346
- stage
347
530
  end
348
531
 
349
- subject { stage.amoeba_dup }
350
-
351
- it 'contains parent association and own associations', :aggregate_failures do
352
- subject
353
- expect { subject.save! }.to change(Listener, :count).by(2).
354
- and change(Specialist, :count).by(1).
355
- and change(CustomRule, :count).by(1)
356
-
357
- expect(subject.title).to eq 'My Stage'
358
- expect(subject.external_id).to be_nil
359
- expect(subject.listeners.find_by(name: 'John')).to_not be_nil
360
- expect(subject.listeners.find_by(name: 'Helen')).to_not be_nil
361
- expect(subject.specialists.find_by(name: 'Jack')).to_not be_nil
362
- expect(subject.custom_rules.first.description).to eq 'Kill all humans'
363
- 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 }
364
558
  end
365
559
 
366
560
  context 'polymorphic' do