mongoid 8.0.11 → 8.0.13

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/lib/config/locales/en.yml +26 -0
  3. data/lib/mongoid/association/depending.rb +8 -4
  4. data/lib/mongoid/association/nested/many.rb +38 -3
  5. data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
  6. data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
  7. data/lib/mongoid/clients/factory.rb +4 -0
  8. data/lib/mongoid/config.rb +62 -0
  9. data/lib/mongoid/contextual/aggregable/memory.rb +5 -2
  10. data/lib/mongoid/contextual/memory.rb +18 -7
  11. data/lib/mongoid/criteria/queryable/mergeable.rb +12 -0
  12. data/lib/mongoid/criteria/queryable/selectable.rb +109 -0
  13. data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
  14. data/lib/mongoid/errors.rb +1 -0
  15. data/lib/mongoid/extensions/hash.rb +27 -1
  16. data/lib/mongoid/field_readable.rb +70 -0
  17. data/lib/mongoid/matchable.rb +6 -1
  18. data/lib/mongoid/matcher/eq_impl_with_regexp.rb +3 -5
  19. data/lib/mongoid/matcher/regex.rb +8 -9
  20. data/lib/mongoid/matcher/regexp_budget.rb +389 -0
  21. data/lib/mongoid/matcher.rb +1 -0
  22. data/lib/mongoid/threaded.rb +3 -0
  23. data/lib/mongoid/version.rb +1 -1
  24. data/lib/mongoid/warnings.rb +1 -0
  25. data/spec/integration/app_spec.rb +8 -0
  26. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
  27. data/spec/integration/dots_and_dollars_spec.rb +12 -2
  28. data/spec/integration/matcher_operator_data/regex.yml +21 -0
  29. data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
  30. data/spec/integration/query_operator_guard_spec.rb +89 -0
  31. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
  32. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
  33. data/spec/mongoid/attributes/nested_spec.rb +204 -10
  34. data/spec/mongoid/clients/factory_spec.rb +30 -0
  35. data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
  36. data/spec/mongoid/contextual/memory_spec.rb +177 -0
  37. data/spec/mongoid/criteria/queryable/extensions/bignum_spec.rb +2 -1
  38. data/spec/mongoid/criteria/queryable/extensions/fixnum_spec.rb +2 -1
  39. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +2 -0
  40. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +235 -0
  41. data/spec/mongoid/criteria_spec.rb +2 -0
  42. data/spec/mongoid/extensions/hash_spec.rb +236 -0
  43. data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
  44. data/spec/shared/CANDIDATE.md +28 -0
  45. data/spec/shared/lib/mrss/spec_organizer.rb +32 -3
  46. data/spec/shared/shlib/server.sh +1 -1
  47. metadata +13 -2
@@ -7,6 +7,33 @@ require_relative './nested_spec_models'
7
7
 
8
8
  describe Mongoid::Attributes::Nested do
9
9
 
10
+ # Expects the enclosing context to define `build_with_id`, taking an id
11
+ # value and returning the document built with it in nested attributes.
12
+ shared_examples 'rejects non-scalar ids' do |klass_name|
13
+ not_found = /not found for class #{klass_name}/
14
+
15
+ context 'when the id is an operator hash' do
16
+ it 'raises a document not found error' do
17
+ expect { build_with_id({ '$ne' => nil }) }
18
+ .to raise_error(Mongoid::Errors::DocumentNotFound, not_found)
19
+ end
20
+ end
21
+
22
+ context 'when the id is an array' do
23
+ it 'raises a document not found error' do
24
+ expect { build_with_id([BSON::ObjectId.new]) }
25
+ .to raise_error(Mongoid::Errors::DocumentNotFound, not_found)
26
+ end
27
+ end
28
+
29
+ context 'when the id is a malformed object id hash' do
30
+ it 'raises a document not found error' do
31
+ expect { build_with_id({ '$oid' => 'junk' }) }
32
+ .to raise_error(Mongoid::Errors::DocumentNotFound, not_found)
33
+ end
34
+ end
35
+ end
36
+
10
37
  describe ".accepts_nested_attributes_for" do
11
38
 
12
39
  context "when the autosave option is not defined" do
@@ -178,6 +205,30 @@ describe Mongoid::Attributes::Nested do
178
205
  end
179
206
  end
180
207
 
208
+ context 'when the id in the attributes is not a scalar' do
209
+ let(:person) { Person.create! }
210
+
211
+ def build_with_id(id)
212
+ person.update!(posts_attributes: { '0' => { id: id, title: 'Crafted' } })
213
+ end
214
+
215
+ context 'when the association is empty' do
216
+ include_examples 'rejects non-scalar ids', 'Post'
217
+ end
218
+
219
+ context 'when the association is not empty' do
220
+ before { person.posts.create!(title: 'Existing') }
221
+
222
+ include_examples 'rejects non-scalar ids', 'Post'
223
+ end
224
+
225
+ context 'when allow_reparenting_via_nested_attributes is true' do
226
+ config_override :allow_reparenting_via_nested_attributes, true
227
+
228
+ include_examples 'rejects non-scalar ids', 'Post'
229
+ end
230
+ end
231
+
181
232
  context "when the relation is a references and referenced in many" do
182
233
 
183
234
  before do
@@ -201,16 +252,100 @@ describe Mongoid::Attributes::Nested do
201
252
  )
202
253
  end
203
254
 
204
- it "sets the nested attributes" do
205
- expect(person.preferences.map(&:name)).to eq([preference.name])
255
+ context 'when allow_reparenting_via_nested_attributes is false' do
256
+ config_override :allow_reparenting_via_nested_attributes, false
257
+
258
+ it 'raises a document not found error' do
259
+ expect { person }.to raise_error(Mongoid::Errors::DocumentNotFound,
260
+ /Document\(s\) not found for class Preference/)
261
+ end
206
262
  end
207
263
 
208
- it "updates attributes of existing document which is added to relation" do
209
- preference_name = 'updated preference'
210
- person = Person.new(
211
- preferences_attributes: { 0 => { id: preference.id, name: preference_name } }
212
- )
213
- expect(person.preferences.map(&:name)).to eq([preference_name])
264
+ context 'when allow_reparenting_via_nested_attributes is true' do
265
+ config_override :allow_reparenting_via_nested_attributes, true
266
+
267
+ it 'sets the nested attributes' do
268
+ expect(person.preferences.map(&:name)).to eq([preference.name])
269
+ end
270
+
271
+ it 'updates attributes of existing document which is added to relation' do
272
+ preference_name = 'updated preference'
273
+ person = Person.new(
274
+ preferences_attributes: { 0 => { id: preference.id, name: preference_name } }
275
+ )
276
+ expect(person.preferences.map(&:name)).to eq([preference_name])
277
+ end
278
+
279
+ context 'when the id does not correspond to an existing document' do
280
+ let(:person) do
281
+ Person.new(
282
+ preferences_attributes: { 0 => { id: BSON::ObjectId.new, name: 'Ghost' } }
283
+ )
284
+ end
285
+
286
+ context 'when raise_not_found_error is true' do
287
+ config_override :raise_not_found_error, true
288
+
289
+ it 'raises a document not found error' do
290
+ expect { person }.to raise_error(Mongoid::Errors::DocumentNotFound,
291
+ /Document\(s\) not found for class Preference/)
292
+ end
293
+ end
294
+
295
+ context 'when raise_not_found_error is false' do
296
+ config_override :raise_not_found_error, false
297
+
298
+ it 'raises a document not found error' do
299
+ expect { person }.to raise_error(Mongoid::Errors::DocumentNotFound,
300
+ /Document\(s\) not found for class Preference/)
301
+ end
302
+ end
303
+ end
304
+ end
305
+
306
+ context 'when _destroy is true for a document not in the relation' do
307
+ before do
308
+ Person.send(:undef_method, :preferences_attributes=)
309
+ Person.accepts_nested_attributes_for :preferences, allow_destroy: true
310
+ end
311
+
312
+ let(:person) do
313
+ Person.new(
314
+ preferences_attributes: { 0 => { id: preference.id, _destroy: '1' } }
315
+ )
316
+ end
317
+
318
+ it 'does not raise UnknownAttribute' do
319
+ expect { person }.not_to raise_error
320
+ end
321
+
322
+ it 'does not add the document to the relation' do
323
+ expect(person.preferences).to be_empty
324
+ end
325
+ end
326
+ end
327
+
328
+ context 'when the id in the attributes is not a scalar' do
329
+ let(:target) { Person.create! }
330
+
331
+ def build_with_id(id)
332
+ target.update!(preferences_attributes: { 0 => { id: id, name: 'Crafted' } })
333
+ end
334
+
335
+ context 'when the association is empty' do
336
+ include_examples 'rejects non-scalar ids', 'Preference'
337
+ end
338
+
339
+ context 'when the association is not empty' do
340
+ before { target.preferences.create!(name: 'Existing') }
341
+
342
+ include_examples 'rejects non-scalar ids', 'Preference'
343
+ end
344
+
345
+ context 'when allow_reparenting_via_nested_attributes is true' do
346
+ config_override :allow_reparenting_via_nested_attributes, true
347
+
348
+ include_examples 'rejects non-scalar ids', 'Preference'
214
349
  end
215
350
  end
216
351
  end
@@ -3015,7 +3150,7 @@ describe Mongoid::Attributes::Nested do
3015
3150
  { "0" =>
3016
3151
  { "id" => BSON::ObjectId.new.to_s, "title" => "Rogue" }
3017
3152
  }
3018
- }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Post with id\(s\)/)
3153
+ }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document not found for class Post/)
3019
3154
  end
3020
3155
  end
3021
3156
  end
@@ -3049,7 +3184,7 @@ describe Mongoid::Attributes::Nested do
3049
3184
  expect {
3050
3185
  person.posts_attributes =
3051
3186
  { "foo" => { "id" => "test", "title" => "Test" } }
3052
- }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Post with id\(s\)/)
3187
+ }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document not found for class Post/)
3053
3188
  end
3054
3189
  end
3055
3190
  end
@@ -4997,4 +5132,63 @@ describe Mongoid::Attributes::Nested do
4997
5132
  end
4998
5133
  end
4999
5134
  end
5135
+
5136
+ context 'when an id in nested attributes belongs to another parent' do
5137
+ let(:victim) { Person.create! }
5138
+ let(:attacker) { Person.create! }
5139
+
5140
+ context 'when the association is a has-many' do
5141
+ before do
5142
+ Person.send(:undef_method, :posts_attributes=)
5143
+ Person.accepts_nested_attributes_for :posts
5144
+ end
5145
+
5146
+ let!(:victim_post) { victim.posts.create!(title: 'Private') }
5147
+
5148
+ it 'does not update the other parent\'s document' do
5149
+ expect { attacker.update!(posts_attributes: { '0' => { id: victim_post.id, title: 'Overwritten' } }) }
5150
+ .to raise_error(Mongoid::Errors::DocumentNotFound)
5151
+ expect(victim_post.reload.title).to eq('Private')
5152
+ end
5153
+
5154
+ it 'does not add the other parent\'s document to the association' do
5155
+ expect { attacker.update!(posts_attributes: { '0' => { id: victim_post.id, title: 'Overwritten' } }) }
5156
+ .to raise_error(Mongoid::Errors::DocumentNotFound)
5157
+ expect(attacker.reload.posts).to be_empty
5158
+ end
5159
+
5160
+ it 'does not update via a direct association assignment' do
5161
+ expect { attacker.update!(posts: { '0' => { id: victim_post.id, title: 'Overwritten' } }) }
5162
+ .to raise_error(Mongoid::Errors::DocumentNotFound)
5163
+ expect(victim_post.reload.title).to eq('Private')
5164
+ end
5165
+ end
5166
+
5167
+ context 'when the association is a has-and-belongs-to-many' do
5168
+ before do
5169
+ Person.send(:undef_method, :preferences_attributes=)
5170
+ Person.accepts_nested_attributes_for :preferences
5171
+ end
5172
+
5173
+ let!(:victim_preference) { victim.preferences.create!(name: 'Private') }
5174
+
5175
+ it 'does not update the other parent\'s document' do
5176
+ expect { attacker.update!(preferences_attributes: { '0' => { id: victim_preference.id, name: 'Overwritten' } }) }
5177
+ .to raise_error(Mongoid::Errors::DocumentNotFound)
5178
+ expect(victim_preference.reload.name).to eq('Private')
5179
+ end
5180
+
5181
+ it 'does not add the other parent\'s document to the association' do
5182
+ expect { attacker.update!(preferences_attributes: { '0' => { id: victim_preference.id, name: 'Overwritten' } }) }
5183
+ .to raise_error(Mongoid::Errors::DocumentNotFound)
5184
+ expect(attacker.reload.preferences).to be_empty
5185
+ end
5186
+
5187
+ it 'does not update via a direct association assignment' do
5188
+ expect { attacker.update!(preferences: { '0' => { id: victim_preference.id, name: 'Overwritten' } }) }
5189
+ .to raise_error(Mongoid::Errors::DocumentNotFound)
5190
+ expect(victim_preference.reload.name).to eq('Private')
5191
+ end
5192
+ end
5193
+ end
5000
5194
  end
@@ -29,6 +29,32 @@ describe Mongoid::Clients::Factory do
29
29
  end
30
30
  end
31
31
 
32
+ shared_examples_for 'includes rails wrapping library' do
33
+ context 'when Rails is available' do
34
+ around do |example|
35
+ original_rails_constant = defined?(::Rails) ? ::Rails : nil
36
+ Object.send(:remove_const, :Rails) if original_rails_constant
37
+
38
+ module ::Rails
39
+ def self.version
40
+ '6.1.0'
41
+ end
42
+ end
43
+
44
+ example.run
45
+
46
+ Object.send(:remove_const, :Rails) if defined?(::Rails)
47
+ Object.const_set(:Rails, original_rails_constant)
48
+ end
49
+
50
+ it 'adds Rails as another wrapping library' do
51
+ expect(client.options[:wrapping_libraries]).to include(
52
+ {'name' => 'Rails', 'version' => '6.1.0'},
53
+ )
54
+ end
55
+ end
56
+ end
57
+
32
58
  describe ".create" do
33
59
 
34
60
  context "when provided a name" do
@@ -88,6 +114,8 @@ describe Mongoid::Clients::Factory do
88
114
  Mongoid::Clients::Factory::MONGOID_WRAPPING_LIBRARY)]
89
115
  end
90
116
 
117
+ it_behaves_like 'includes rails wrapping library'
118
+
91
119
  context 'when configuration specifies a wrapping library' do
92
120
 
93
121
  let(:config) do
@@ -109,6 +137,8 @@ describe Mongoid::Clients::Factory do
109
137
  {'name' => 'Foo'},
110
138
  ]
111
139
  end
140
+
141
+ it_behaves_like 'includes rails wrapping library'
112
142
  end
113
143
  end
114
144
 
@@ -570,4 +570,95 @@ describe Mongoid::Contextual::Aggregable::Memory do
570
570
  end
571
571
  end
572
572
  end
573
+
574
+ context 'when the field name is a method name' do
575
+ let!(:depeche) do
576
+ Band.create!(name: 'Depeche Mode', likes: 1000)
577
+ end
578
+
579
+ let(:criteria) do
580
+ Band.all.tap do |crit|
581
+ crit.documents = [ depeche ]
582
+ end
583
+ end
584
+
585
+ it 'does not call the method for sum' do
586
+ expect(depeche).not_to receive(:delete)
587
+ expect(context.sum(:delete)).to eq(0)
588
+ end
589
+
590
+ it 'does not call the method for min' do
591
+ expect(depeche).not_to receive(:delete)
592
+ expect(context.min(:delete)).to be_nil
593
+ end
594
+
595
+ it 'does not call the method for max' do
596
+ expect(depeche).not_to receive(:delete)
597
+ expect(context.max(:delete)).to be_nil
598
+ end
599
+
600
+ it 'does not call the method for avg' do
601
+ expect(depeche).not_to receive(:delete)
602
+ expect(context.avg(:delete)).to be_nil
603
+ end
604
+
605
+ it 'does not call the method for aggregates' do
606
+ expect(depeche).not_to receive(:delete)
607
+ expect(context.aggregates(:delete))
608
+ .to eq('count' => 0, 'avg' => nil, 'max' => nil, 'min' => nil, 'sum' => 0)
609
+ end
610
+
611
+ it 'does not call a private method for avg' do
612
+ expect(depeche).not_to receive(:exit)
613
+ expect(context.avg(:exit)).to be_nil
614
+ end
615
+ end
616
+
617
+ context 'when the field is not defined' do
618
+ let!(:depeche) do
619
+ Band.create!(name: 'Depeche Mode', likes: 1000)
620
+ end
621
+
622
+ let(:criteria) do
623
+ Band.all.tap do |crit|
624
+ crit.documents = [ depeche ]
625
+ end
626
+ end
627
+
628
+ it 'returns zero for sum' do
629
+ expect(context.sum(:not_a_field)).to eq(0)
630
+ end
631
+
632
+ it 'returns nil for min' do
633
+ expect(context.min(:not_a_field)).to be_nil
634
+ end
635
+
636
+ it 'returns nil for max' do
637
+ expect(context.max(:not_a_field)).to be_nil
638
+ end
639
+
640
+ it 'returns nil for avg' do
641
+ expect(context.avg(:not_a_field)).to be_nil
642
+ end
643
+ end
644
+
645
+ context 'when the field is the foreign key of a many to many association' do
646
+ let!(:preference) do
647
+ Preference.create!(name: 'nature')
648
+ end
649
+
650
+ let!(:person) do
651
+ Person.new(preferences: [ preference ])
652
+ end
653
+
654
+ let(:criteria) do
655
+ Person.all.tap do |crit|
656
+ crit.documents = [ person ]
657
+ end
658
+ end
659
+
660
+ it 'aggregates the stored ids' do
661
+ expect(context.max(:preference_ids)).to eq([ preference.id ])
662
+ end
663
+ end
573
664
  end
@@ -633,6 +633,27 @@ describe Mongoid::Contextual::Memory do
633
633
  end
634
634
  end
635
635
  end
636
+
637
+ context 'when the field name is a method name' do
638
+ let!(:person) do
639
+ Person.create!(ssn: 'secret-ssn')
640
+ end
641
+
642
+ let!(:address) do
643
+ person.addresses.create!(street: 'hobrecht')
644
+ end
645
+
646
+ let(:criteria) do
647
+ Address.all.tap do |crit|
648
+ crit.documents = [ address ]
649
+ end
650
+ end
651
+
652
+ it 'does not call the method' do
653
+ expect(address).not_to receive(:destroy)
654
+ expect(context.distinct(:destroy)).to eq([ nil ])
655
+ end
656
+ end
636
657
  end
637
658
 
638
659
  describe "#each" do
@@ -1743,6 +1764,116 @@ describe Mongoid::Contextual::Memory do
1743
1764
  end
1744
1765
  end
1745
1766
  end
1767
+
1768
+ context 'when plucking a dynamic attribute' do
1769
+ let(:criteria) do
1770
+ Band.all.tap do |crit|
1771
+ crit.documents = [ Band.create!(name: 'Depeche Mode', mood: 'dark') ]
1772
+ end
1773
+ end
1774
+
1775
+ it 'returns the value from the attributes' do
1776
+ expect(context.pluck(:mood)).to eq([ 'dark' ])
1777
+ end
1778
+ end
1779
+
1780
+ context 'when the field name is a method name' do
1781
+ let!(:person) do
1782
+ Person.create!(ssn: 'secret-ssn')
1783
+ end
1784
+
1785
+ let!(:address) do
1786
+ person.addresses.create!(street: 'hobrecht')
1787
+ end
1788
+
1789
+ let(:criteria) do
1790
+ Address.all.tap do |crit|
1791
+ crit.documents = [ address ]
1792
+ end
1793
+ end
1794
+
1795
+ it 'does not call the method' do
1796
+ expect(address).not_to receive(:destroy)
1797
+ expect(context.pluck(:destroy)).to eq([ nil ])
1798
+ end
1799
+
1800
+ it 'does not return the attributes hash' do
1801
+ expect(context.pluck(:attributes)).to eq([ nil ])
1802
+ end
1803
+
1804
+ it 'does not traverse to the parent document' do
1805
+ expect(context.pluck('_root.ssn')).to eq([ nil ])
1806
+ end
1807
+
1808
+ it 'does not destroy the parent document' do
1809
+ expect(context.pluck('_root.destroy')).to eq([ nil ])
1810
+ expect(Person.where(_id: person.id).count).to eq(1)
1811
+ end
1812
+ end
1813
+
1814
+ context 'when plucking a belongs_to association' do
1815
+ let!(:band) do
1816
+ Band.create!(name: 'Depeche Mode')
1817
+ end
1818
+
1819
+ let!(:artist) do
1820
+ Artist.create!(band: band)
1821
+ end
1822
+
1823
+ let(:criteria) do
1824
+ Artist.all.tap do |crit|
1825
+ crit.documents = [ artist ]
1826
+ end
1827
+ end
1828
+
1829
+ it 'traverses to the associated document' do
1830
+ expect(context.pluck('band.name')).to eq([ 'Depeche Mode' ])
1831
+ end
1832
+
1833
+ it 'returns the document for the association name' do
1834
+ expect(context.pluck(:band)).to eq([ band ])
1835
+ end
1836
+
1837
+ it 'returns the id for the foreign key' do
1838
+ expect(context.pluck(:band_id)).to eq([ band.id ])
1839
+ end
1840
+ end
1841
+
1842
+ context 'when plucking the foreign key of a many to many association' do
1843
+ let!(:preference) do
1844
+ Preference.create!(name: 'nature')
1845
+ end
1846
+
1847
+ let!(:person) do
1848
+ Person.create!(preferences: [ preference ])
1849
+ end
1850
+
1851
+ let(:criteria) do
1852
+ Person.all.tap do |crit|
1853
+ crit.documents = [ person ]
1854
+ end
1855
+ end
1856
+
1857
+ it 'returns the stored ids' do
1858
+ expect(context.pluck(:preference_ids)).to eq([ [ preference.id ] ])
1859
+ end
1860
+ end
1861
+
1862
+ context 'when the field path has an empty segment' do
1863
+ let!(:band) do
1864
+ Band.create!(name: 'Depeche Mode', label: Label.new(name: 'Mute'))
1865
+ end
1866
+
1867
+ let(:criteria) do
1868
+ Band.all.tap do |crit|
1869
+ crit.documents = [ band ]
1870
+ end
1871
+ end
1872
+
1873
+ it 'returns nil' do
1874
+ expect(context.pluck('label..name')).to eq([ nil ])
1875
+ end
1876
+ end
1746
1877
  end
1747
1878
 
1748
1879
  describe "#pick" do
@@ -1803,6 +1934,27 @@ describe Mongoid::Contextual::Memory do
1803
1934
  expect(picked).to be_nil
1804
1935
  end
1805
1936
  end
1937
+
1938
+ context 'when the field name is a method name' do
1939
+ let!(:person) do
1940
+ Person.create!(ssn: 'secret-ssn')
1941
+ end
1942
+
1943
+ let!(:address) do
1944
+ person.addresses.create!(street: 'hobrecht')
1945
+ end
1946
+
1947
+ let(:criteria) do
1948
+ Address.all.tap do |crit|
1949
+ crit.documents = [ address ]
1950
+ end
1951
+ end
1952
+
1953
+ it 'does not call the method' do
1954
+ expect(address).not_to receive(:destroy)
1955
+ expect(context.pick(:destroy)).to be_nil
1956
+ end
1957
+ end
1806
1958
  end
1807
1959
 
1808
1960
  describe "#tally" do
@@ -2254,6 +2406,31 @@ describe Mongoid::Contextual::Memory do
2254
2406
  )
2255
2407
  end
2256
2408
  end
2409
+
2410
+ context 'when the field name is a method name' do
2411
+ let!(:person) do
2412
+ Person.create!(ssn: 'secret-ssn')
2413
+ end
2414
+
2415
+ let!(:address) do
2416
+ person.addresses.create!(street: 'hobrecht')
2417
+ end
2418
+
2419
+ let(:criteria) do
2420
+ Address.all.tap do |crit|
2421
+ crit.documents = [ address ]
2422
+ end
2423
+ end
2424
+
2425
+ it 'does not call the method' do
2426
+ expect(address).not_to receive(:destroy)
2427
+ expect(context.tally(:destroy)).to eq(nil => 1)
2428
+ end
2429
+
2430
+ it 'does not disclose the parent document fields' do
2431
+ expect(context.tally('_root.ssn')).to eq(nil => 1)
2432
+ end
2433
+ end
2257
2434
  end
2258
2435
 
2259
2436
  describe '#inc' do
@@ -2,7 +2,8 @@
2
2
 
3
3
  require "spec_helper"
4
4
 
5
- describe Bignum do
5
+ describe 'Bignum' do
6
+ ruby_version_lt "2.4"
6
7
 
7
8
  describe ".evolve" do
8
9
 
@@ -2,7 +2,8 @@
2
2
 
3
3
  require "spec_helper"
4
4
 
5
- describe Fixnum do
5
+ describe 'Fixnum' do
6
+ ruby_version_lt "2.4"
6
7
 
7
8
  describe ".evolve" do
8
9
 
@@ -1829,6 +1829,8 @@ describe Mongoid::Criteria::Queryable::Selectable do
1829
1829
  end
1830
1830
 
1831
1831
  context 'when the following criteria uses string were form' do
1832
+ # String criteria compile to $where, which requires the opt-in.
1833
+ config_override :allow_unsafe_query_operators, true
1832
1834
 
1833
1835
  let(:selection) do
1834
1836
  query.not.where('hello world')