mongoid 7.6.1 → 7.6.2

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 (45) 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 +102 -3
  7. data/lib/mongoid/config.rb +62 -0
  8. data/lib/mongoid/contextual/aggregable/memory.rb +8 -2
  9. data/lib/mongoid/contextual/memory.rb +19 -8
  10. data/lib/mongoid/criteria/queryable/mergeable.rb +12 -0
  11. data/lib/mongoid/criteria/queryable/selectable.rb +109 -0
  12. data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
  13. data/lib/mongoid/errors.rb +1 -0
  14. data/lib/mongoid/field_readable.rb +70 -0
  15. data/lib/mongoid/matchable.rb +6 -1
  16. data/lib/mongoid/matcher/eq_impl_with_regexp.rb +3 -5
  17. data/lib/mongoid/matcher/regex.rb +8 -9
  18. data/lib/mongoid/matcher/regexp_budget.rb +389 -0
  19. data/lib/mongoid/matcher.rb +1 -0
  20. data/lib/mongoid/threaded.rb +3 -0
  21. data/lib/mongoid/version.rb +1 -1
  22. data/lib/mongoid/warnings.rb +1 -0
  23. data/spec/integration/app_spec.rb +8 -0
  24. data/spec/integration/matcher_operator_data/regex.yml +21 -0
  25. data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
  26. data/spec/integration/query_operator_guard_spec.rb +89 -0
  27. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
  28. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
  29. data/spec/mongoid/attributes/nested_spec.rb +202 -8
  30. data/spec/mongoid/contextual/aggregable/memory_spec.rb +98 -0
  31. data/spec/mongoid/contextual/memory_spec.rb +196 -0
  32. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +2 -0
  33. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +200 -0
  34. data/spec/mongoid/criteria_spec.rb +2 -0
  35. data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
  36. data/spec/shared/lib/mrss/cluster_config.rb +5 -40
  37. data/spec/shared/lib/mrss/constraints.rb +7 -46
  38. data/spec/shared/lib/mrss/docker_runner.rb +10 -21
  39. data/spec/shared/lib/mrss/eg_config_utils.rb +0 -31
  40. data/spec/shared/lib/mrss/release/candidate.rb +18 -12
  41. data/spec/shared/lib/mrss/server_version_registry.rb +1 -5
  42. data/spec/shared/share/Dockerfile.erb +6 -8
  43. data/spec/shared/shlib/server.sh +0 -50
  44. data/spec/shared/shlib/set_env.sh +4 -10
  45. metadata +11 -2
@@ -4,6 +4,33 @@ require "spec_helper"
4
4
 
5
5
  describe Mongoid::Attributes::Nested do
6
6
 
7
+ # Expects the enclosing context to define `build_with_id`, taking an id
8
+ # value and returning the document built with it in nested attributes.
9
+ shared_examples 'rejects non-scalar ids' do |klass_name|
10
+ not_found = /not found for class #{klass_name}/
11
+
12
+ context 'when the id is an operator hash' do
13
+ it 'raises a document not found error' do
14
+ expect { build_with_id({ '$ne' => nil }) }
15
+ .to raise_error(Mongoid::Errors::DocumentNotFound, not_found)
16
+ end
17
+ end
18
+
19
+ context 'when the id is an array' do
20
+ it 'raises a document not found error' do
21
+ expect { build_with_id([BSON::ObjectId.new]) }
22
+ .to raise_error(Mongoid::Errors::DocumentNotFound, not_found)
23
+ end
24
+ end
25
+
26
+ context 'when the id is a malformed object id hash' do
27
+ it 'raises a document not found error' do
28
+ expect { build_with_id({ '$oid' => 'junk' }) }
29
+ .to raise_error(Mongoid::Errors::DocumentNotFound, not_found)
30
+ end
31
+ end
32
+ end
33
+
7
34
  describe ".accepts_nested_attributes_for" do
8
35
 
9
36
  context "when the autosave option is not defined" do
@@ -175,6 +202,30 @@ describe Mongoid::Attributes::Nested do
175
202
  end
176
203
  end
177
204
 
205
+ context 'when the id in the attributes is not a scalar' do
206
+ let(:person) { Person.create! }
207
+
208
+ def build_with_id(id)
209
+ person.update!(posts_attributes: { '0' => { id: id, title: 'Crafted' } })
210
+ end
211
+
212
+ context 'when the association is empty' do
213
+ include_examples 'rejects non-scalar ids', 'Post'
214
+ end
215
+
216
+ context 'when the association is not empty' do
217
+ before { person.posts.create!(title: 'Existing') }
218
+
219
+ include_examples 'rejects non-scalar ids', 'Post'
220
+ end
221
+
222
+ context 'when allow_reparenting_via_nested_attributes is true' do
223
+ config_override :allow_reparenting_via_nested_attributes, true
224
+
225
+ include_examples 'rejects non-scalar ids', 'Post'
226
+ end
227
+ end
228
+
178
229
  context "when the relation is a references and referenced in many" do
179
230
 
180
231
  before do
@@ -198,16 +249,100 @@ describe Mongoid::Attributes::Nested do
198
249
  )
199
250
  end
200
251
 
201
- it "sets the nested attributes" do
202
- expect(person.preferences.map(&:name)).to eq([preference.name])
252
+ context 'when allow_reparenting_via_nested_attributes is false' do
253
+ config_override :allow_reparenting_via_nested_attributes, false
254
+
255
+ it 'raises a document not found error' do
256
+ expect { person }.to raise_error(Mongoid::Errors::DocumentNotFound,
257
+ /Document\(s\) not found for class Preference/)
258
+ end
203
259
  end
204
260
 
205
- it "updates attributes of existing document which is added to relation" do
206
- preference_name = 'updated preference'
207
- person = Person.new(
208
- preferences_attributes: { 0 => { id: preference.id, name: preference_name } }
209
- )
210
- expect(person.preferences.map(&:name)).to eq([preference_name])
261
+ context 'when allow_reparenting_via_nested_attributes is true' do
262
+ config_override :allow_reparenting_via_nested_attributes, true
263
+
264
+ it 'sets the nested attributes' do
265
+ expect(person.preferences.map(&:name)).to eq([preference.name])
266
+ end
267
+
268
+ it 'updates attributes of existing document which is added to relation' do
269
+ preference_name = 'updated preference'
270
+ person = Person.new(
271
+ preferences_attributes: { 0 => { id: preference.id, name: preference_name } }
272
+ )
273
+ expect(person.preferences.map(&:name)).to eq([preference_name])
274
+ end
275
+
276
+ context 'when the id does not correspond to an existing document' do
277
+ let(:person) do
278
+ Person.new(
279
+ preferences_attributes: { 0 => { id: BSON::ObjectId.new, name: 'Ghost' } }
280
+ )
281
+ end
282
+
283
+ context 'when raise_not_found_error is true' do
284
+ config_override :raise_not_found_error, true
285
+
286
+ it 'raises a document not found error' do
287
+ expect { person }.to raise_error(Mongoid::Errors::DocumentNotFound,
288
+ /Document\(s\) not found for class Preference/)
289
+ end
290
+ end
291
+
292
+ context 'when raise_not_found_error is false' do
293
+ config_override :raise_not_found_error, false
294
+
295
+ it 'raises a document not found error' do
296
+ expect { person }.to raise_error(Mongoid::Errors::DocumentNotFound,
297
+ /Document\(s\) not found for class Preference/)
298
+ end
299
+ end
300
+ end
301
+ end
302
+
303
+ context 'when _destroy is true for a document not in the relation' do
304
+ before do
305
+ Person.send(:undef_method, :preferences_attributes=)
306
+ Person.accepts_nested_attributes_for :preferences, allow_destroy: true
307
+ end
308
+
309
+ let(:person) do
310
+ Person.new(
311
+ preferences_attributes: { 0 => { id: preference.id, _destroy: '1' } }
312
+ )
313
+ end
314
+
315
+ it 'does not raise UnknownAttribute' do
316
+ expect { person }.not_to raise_error
317
+ end
318
+
319
+ it 'does not add the document to the relation' do
320
+ expect(person.preferences).to be_empty
321
+ end
322
+ end
323
+ end
324
+
325
+ context 'when the id in the attributes is not a scalar' do
326
+ let(:target) { Person.create! }
327
+
328
+ def build_with_id(id)
329
+ target.update!(preferences_attributes: { 0 => { id: id, name: 'Crafted' } })
330
+ end
331
+
332
+ context 'when the association is empty' do
333
+ include_examples 'rejects non-scalar ids', 'Preference'
334
+ end
335
+
336
+ context 'when the association is not empty' do
337
+ before { target.preferences.create!(name: 'Existing') }
338
+
339
+ include_examples 'rejects non-scalar ids', 'Preference'
340
+ end
341
+
342
+ context 'when allow_reparenting_via_nested_attributes is true' do
343
+ config_override :allow_reparenting_via_nested_attributes, true
344
+
345
+ include_examples 'rejects non-scalar ids', 'Preference'
211
346
  end
212
347
  end
213
348
  end
@@ -4928,4 +5063,63 @@ describe Mongoid::Attributes::Nested do
4928
5063
  end
4929
5064
  end
4930
5065
  end
5066
+
5067
+ context 'when an id in nested attributes belongs to another parent' do
5068
+ let(:victim) { Person.create! }
5069
+ let(:attacker) { Person.create! }
5070
+
5071
+ context 'when the association is a has-many' do
5072
+ before do
5073
+ Person.send(:undef_method, :posts_attributes=)
5074
+ Person.accepts_nested_attributes_for :posts
5075
+ end
5076
+
5077
+ let!(:victim_post) { victim.posts.create!(title: 'Private') }
5078
+
5079
+ it 'does not update the other parent\'s document' do
5080
+ expect { attacker.update!(posts_attributes: { '0' => { id: victim_post.id, title: 'Overwritten' } }) }
5081
+ .to raise_error(Mongoid::Errors::DocumentNotFound)
5082
+ expect(victim_post.reload.title).to eq('Private')
5083
+ end
5084
+
5085
+ it 'does not add the other parent\'s document to the association' do
5086
+ expect { attacker.update!(posts_attributes: { '0' => { id: victim_post.id, title: 'Overwritten' } }) }
5087
+ .to raise_error(Mongoid::Errors::DocumentNotFound)
5088
+ expect(attacker.reload.posts).to be_empty
5089
+ end
5090
+
5091
+ it 'does not update via a direct association assignment' do
5092
+ expect { attacker.update!(posts: { '0' => { id: victim_post.id, title: 'Overwritten' } }) }
5093
+ .to raise_error(Mongoid::Errors::DocumentNotFound)
5094
+ expect(victim_post.reload.title).to eq('Private')
5095
+ end
5096
+ end
5097
+
5098
+ context 'when the association is a has-and-belongs-to-many' do
5099
+ before do
5100
+ Person.send(:undef_method, :preferences_attributes=)
5101
+ Person.accepts_nested_attributes_for :preferences
5102
+ end
5103
+
5104
+ let!(:victim_preference) { victim.preferences.create!(name: 'Private') }
5105
+
5106
+ it 'does not update the other parent\'s document' do
5107
+ expect { attacker.update!(preferences_attributes: { '0' => { id: victim_preference.id, name: 'Overwritten' } }) }
5108
+ .to raise_error(Mongoid::Errors::DocumentNotFound)
5109
+ expect(victim_preference.reload.name).to eq('Private')
5110
+ end
5111
+
5112
+ it 'does not add the other parent\'s document to the association' do
5113
+ expect { attacker.update!(preferences_attributes: { '0' => { id: victim_preference.id, name: 'Overwritten' } }) }
5114
+ .to raise_error(Mongoid::Errors::DocumentNotFound)
5115
+ expect(attacker.reload.preferences).to be_empty
5116
+ end
5117
+
5118
+ it 'does not update via a direct association assignment' do
5119
+ expect { attacker.update!(preferences: { '0' => { id: victim_preference.id, name: 'Overwritten' } }) }
5120
+ .to raise_error(Mongoid::Errors::DocumentNotFound)
5121
+ expect(victim_preference.reload.name).to eq('Private')
5122
+ end
5123
+ end
5124
+ end
4931
5125
  end
@@ -4,6 +4,10 @@ require "spec_helper"
4
4
 
5
5
  describe Mongoid::Contextual::Aggregable::Memory do
6
6
 
7
+ let(:context) do
8
+ Mongoid::Contextual::Memory.new(criteria)
9
+ end
10
+
7
11
  describe "#aggregates" do
8
12
  let(:context) do
9
13
  Mongoid::Contextual::Memory.new(criteria)
@@ -332,4 +336,98 @@ describe Mongoid::Contextual::Aggregable::Memory do
332
336
  end
333
337
  end
334
338
  end
339
+
340
+ context "when the field name is a method name" do
341
+
342
+ let!(:depeche) do
343
+ Band.create!(name: "Depeche Mode", likes: 1000)
344
+ end
345
+
346
+ let(:criteria) do
347
+ Band.all.tap do |crit|
348
+ crit.documents = [ depeche ]
349
+ end
350
+ end
351
+
352
+ it "does not call the method for sum" do
353
+ expect(depeche).not_to receive(:delete)
354
+ expect(context.sum(:delete)).to eq(0)
355
+ end
356
+
357
+ it "does not call the method for min" do
358
+ expect(depeche).not_to receive(:delete)
359
+ expect(context.min(:delete)).to be_nil
360
+ end
361
+
362
+ it "does not call the method for max" do
363
+ expect(depeche).not_to receive(:delete)
364
+ expect(context.max(:delete)).to be_nil
365
+ end
366
+
367
+ it "does not call the method for avg" do
368
+ expect(depeche).not_to receive(:delete)
369
+ expect(context.avg(:delete)).to eq(0.0)
370
+ end
371
+
372
+ it "does not call the method for aggregates" do
373
+ expect(depeche).not_to receive(:delete)
374
+ expect(context.aggregates(:delete))
375
+ .to eq("count" => 0, "avg" => 0.0, "max" => nil, "min" => nil, "sum" => 0)
376
+ end
377
+
378
+ it "does not call a private method for avg" do
379
+ expect(depeche).not_to receive(:exit)
380
+ expect(context.avg(:exit)).to eq(0.0)
381
+ end
382
+ end
383
+
384
+ context "when the field is not defined" do
385
+
386
+ let!(:depeche) do
387
+ Band.create!(name: "Depeche Mode", likes: 1000)
388
+ end
389
+
390
+ let(:criteria) do
391
+ Band.all.tap do |crit|
392
+ crit.documents = [ depeche ]
393
+ end
394
+ end
395
+
396
+ it "returns zero for sum" do
397
+ expect(context.sum(:not_a_field)).to eq(0)
398
+ end
399
+
400
+ it "returns nil for min" do
401
+ expect(context.min(:not_a_field)).to be_nil
402
+ end
403
+
404
+ it "returns nil for max" do
405
+ expect(context.max(:not_a_field)).to be_nil
406
+ end
407
+
408
+ it "returns zero for avg" do
409
+ expect(context.avg(:not_a_field)).to eq(0.0)
410
+ end
411
+ end
412
+
413
+ context "when the field is the foreign key of a many to many association" do
414
+
415
+ let!(:preference) do
416
+ Preference.create!(name: "nature")
417
+ end
418
+
419
+ let!(:person) do
420
+ Person.new(preferences: [ preference ])
421
+ end
422
+
423
+ let(:criteria) do
424
+ Person.all.tap do |crit|
425
+ crit.documents = [ person ]
426
+ end
427
+ end
428
+
429
+ it "aggregates the stored ids" do
430
+ expect(context.max(:preference_ids)).to eq([ preference.id ])
431
+ end
432
+ end
335
433
  end
@@ -438,6 +438,32 @@ describe Mongoid::Contextual::Memory do
438
438
  }.to raise_exception(Mongoid::Errors::InMemoryCollationNotSupported)
439
439
  end
440
440
  end
441
+
442
+ context "when the field name is a method name" do
443
+
444
+ let!(:person) do
445
+ Person.create!(ssn: "secret-ssn")
446
+ end
447
+
448
+ let!(:address) do
449
+ person.addresses.create!(street: "hobrecht")
450
+ end
451
+
452
+ let(:criteria) do
453
+ Address.all.tap do |crit|
454
+ crit.documents = [ address ]
455
+ end
456
+ end
457
+
458
+ let(:context) do
459
+ described_class.new(criteria)
460
+ end
461
+
462
+ it "does not call the method" do
463
+ expect(address).not_to receive(:destroy)
464
+ expect(context.distinct(:destroy)).to eq([ nil ])
465
+ end
466
+ end
441
467
  end
442
468
 
443
469
  context "when legacy_pluck_distinct is false" do
@@ -632,6 +658,28 @@ describe Mongoid::Contextual::Memory do
632
658
  expect(context.distinct("label.sales")).to eq([ BigDecimal("1E2") ])
633
659
  end
634
660
  end
661
+
662
+ context "when the field name is a method name" do
663
+
664
+ let!(:person) do
665
+ Person.create!(ssn: "secret-ssn")
666
+ end
667
+
668
+ let!(:address) do
669
+ person.addresses.create!(street: "hobrecht")
670
+ end
671
+
672
+ let(:criteria) do
673
+ Address.all.tap do |crit|
674
+ crit.documents = [ address ]
675
+ end
676
+ end
677
+
678
+ it "does not call the method" do
679
+ expect(address).not_to receive(:destroy)
680
+ expect(context.distinct(:destroy)).to eq([ nil ])
681
+ end
682
+ end
635
683
  end
636
684
  end
637
685
 
@@ -1747,6 +1795,121 @@ describe Mongoid::Contextual::Memory do
1747
1795
  ])
1748
1796
  end
1749
1797
  end
1798
+
1799
+ context "when plucking a dynamic attribute" do
1800
+
1801
+ let(:criteria) do
1802
+ Band.all.tap do |crit|
1803
+ crit.documents = [ Band.create!(name: "Depeche Mode", mood: "dark") ]
1804
+ end
1805
+ end
1806
+
1807
+ it "returns the value from the attributes" do
1808
+ expect(context.pluck(:mood)).to eq([ "dark" ])
1809
+ end
1810
+ end
1811
+
1812
+ context "when the field name is a method name" do
1813
+
1814
+ let!(:person) do
1815
+ Person.create!(ssn: "secret-ssn")
1816
+ end
1817
+
1818
+ let!(:address) do
1819
+ person.addresses.create!(street: "hobrecht")
1820
+ end
1821
+
1822
+ let(:criteria) do
1823
+ Address.all.tap do |crit|
1824
+ crit.documents = [ address ]
1825
+ end
1826
+ end
1827
+
1828
+ it "does not call the method" do
1829
+ expect(address).not_to receive(:destroy)
1830
+ expect(context.pluck(:destroy)).to eq([ nil ])
1831
+ end
1832
+
1833
+ it "does not return the attributes hash" do
1834
+ expect(context.pluck(:attributes)).to eq([ nil ])
1835
+ end
1836
+
1837
+ it "does not traverse to the parent document" do
1838
+ expect(context.pluck("_root.ssn")).to eq([ nil ])
1839
+ end
1840
+
1841
+ it "does not destroy the parent document" do
1842
+ expect(context.pluck("_root.destroy")).to eq([ nil ])
1843
+ expect(Person.where(_id: person.id).count).to eq(1)
1844
+ end
1845
+ end
1846
+
1847
+ context "when plucking a belongs_to association" do
1848
+
1849
+ let!(:band) do
1850
+ Band.create!(name: "Depeche Mode")
1851
+ end
1852
+
1853
+ let!(:artist) do
1854
+ Artist.create!(band: band)
1855
+ end
1856
+
1857
+ let(:criteria) do
1858
+ Artist.all.tap do |crit|
1859
+ crit.documents = [ artist ]
1860
+ end
1861
+ end
1862
+
1863
+ it "traverses to the associated document" do
1864
+ expect(context.pluck("band.name")).to eq([ "Depeche Mode" ])
1865
+ end
1866
+
1867
+ it "returns the document for the association name" do
1868
+ expect(context.pluck(:band)).to eq([ band ])
1869
+ end
1870
+
1871
+ it "returns the id for the foreign key" do
1872
+ expect(context.pluck(:band_id)).to eq([ band.id ])
1873
+ end
1874
+ end
1875
+
1876
+ context "when plucking the foreign key of a many to many association" do
1877
+
1878
+ let!(:preference) do
1879
+ Preference.create!(name: "nature")
1880
+ end
1881
+
1882
+ let!(:person) do
1883
+ Person.create!(preferences: [ preference ])
1884
+ end
1885
+
1886
+ let(:criteria) do
1887
+ Person.all.tap do |crit|
1888
+ crit.documents = [ person ]
1889
+ end
1890
+ end
1891
+
1892
+ it "returns the stored ids" do
1893
+ expect(context.pluck(:preference_ids)).to eq([ [ preference.id ] ])
1894
+ end
1895
+ end
1896
+
1897
+ context "when the field path has an empty segment" do
1898
+
1899
+ let!(:band) do
1900
+ Band.create!(name: "Depeche Mode", label: Label.new(name: "Mute"))
1901
+ end
1902
+
1903
+ let(:criteria) do
1904
+ Band.all.tap do |crit|
1905
+ crit.documents = [ band ]
1906
+ end
1907
+ end
1908
+
1909
+ it "returns nil" do
1910
+ expect(context.pluck("label..name")).to eq([ nil ])
1911
+ end
1912
+ end
1750
1913
  end
1751
1914
  end
1752
1915
 
@@ -1769,6 +1932,39 @@ describe Mongoid::Contextual::Memory do
1769
1932
  end
1770
1933
  end
1771
1934
 
1935
+ describe "#tally" do
1936
+
1937
+ let(:context) do
1938
+ described_class.new(criteria)
1939
+ end
1940
+
1941
+ context "when the field name is a method name" do
1942
+
1943
+ let!(:person) do
1944
+ Person.create!(ssn: "secret-ssn")
1945
+ end
1946
+
1947
+ let!(:address) do
1948
+ person.addresses.create!(street: "hobrecht")
1949
+ end
1950
+
1951
+ let(:criteria) do
1952
+ Address.all.tap do |crit|
1953
+ crit.documents = [ address ]
1954
+ end
1955
+ end
1956
+
1957
+ it "does not call the method" do
1958
+ expect(address).not_to receive(:destroy)
1959
+ expect(context.tally(:destroy)).to eq(nil => 1)
1960
+ end
1961
+
1962
+ it "does not disclose the parent document fields" do
1963
+ expect(context.tally("_root.ssn")).to eq(nil => 1)
1964
+ end
1965
+ end
1966
+ end
1967
+
1772
1968
  describe "#skip" do
1773
1969
 
1774
1970
  let(:hobrecht) do
@@ -1801,6 +1801,8 @@ describe Mongoid::Criteria::Queryable::Selectable do
1801
1801
  end
1802
1802
 
1803
1803
  context 'when the following criteria uses string were form' do
1804
+ # String criteria compile to $where, which requires the opt-in.
1805
+ config_override :allow_unsafe_query_operators, true
1804
1806
 
1805
1807
  let(:selection) do
1806
1808
  query.not.where('hello world')