mongoid 9.0.11 → 9.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/lib/config/locales/en.yml +65 -0
  3. data/lib/mongoid/association/depending.rb +8 -4
  4. data/lib/mongoid/association/embedded/batchable.rb +8 -0
  5. data/lib/mongoid/association/nested/many.rb +34 -5
  6. data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
  7. data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
  8. data/lib/mongoid/association/relatable.rb +17 -0
  9. data/lib/mongoid/clients/factory.rb +2 -0
  10. data/lib/mongoid/collection_configurable.rb +8 -0
  11. data/lib/mongoid/config/encryption.rb +171 -22
  12. data/lib/mongoid/config.rb +56 -9
  13. data/lib/mongoid/contextual/aggregable/memory.rb +5 -2
  14. data/lib/mongoid/contextual/memory.rb +18 -7
  15. data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
  16. data/lib/mongoid/criteria/queryable/selectable.rb +109 -0
  17. data/lib/mongoid/encryptable.rb +73 -0
  18. data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
  19. data/lib/mongoid/errors/invalid_encryption_schema.rb +27 -0
  20. data/lib/mongoid/errors/no_encryption_schema.rb +30 -0
  21. data/lib/mongoid/errors.rb +3 -0
  22. data/lib/mongoid/field_readable.rb +70 -0
  23. data/lib/mongoid/indexable.rb +39 -27
  24. data/lib/mongoid/matchable.rb +6 -1
  25. data/lib/mongoid/matcher/eq_impl_with_regexp.rb +4 -9
  26. data/lib/mongoid/matcher/regex.rb +9 -13
  27. data/lib/mongoid/matcher/regexp_budget.rb +383 -0
  28. data/lib/mongoid/matcher.rb +1 -0
  29. data/lib/mongoid/persistence_context.rb +77 -0
  30. data/lib/mongoid/search_indexable.rb +19 -8
  31. data/lib/mongoid/tasks/database.rb +33 -16
  32. data/lib/mongoid/threaded.rb +37 -0
  33. data/lib/mongoid/version.rb +1 -1
  34. data/lib/mongoid/warnings.rb +4 -0
  35. data/spec/integration/app_spec.rb +7 -0
  36. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
  37. data/spec/integration/atomic/modifiers_spec.rb +57 -0
  38. data/spec/integration/dots_and_dollars_spec.rb +12 -2
  39. data/spec/integration/encryption_spec.rb +298 -0
  40. data/spec/integration/matcher_operator_data/regex.yml +21 -0
  41. data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
  42. data/spec/integration/query_operator_guard_spec.rb +89 -0
  43. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
  44. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
  45. data/spec/mongoid/attributes/nested_spec.rb +204 -10
  46. data/spec/mongoid/clients/factory_spec.rb +36 -0
  47. data/spec/mongoid/config/defaults_spec.rb +18 -0
  48. data/spec/mongoid/config/encryption_spec.rb +451 -0
  49. data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
  50. data/spec/mongoid/contextual/memory_spec.rb +177 -0
  51. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +2 -0
  52. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +200 -0
  53. data/spec/mongoid/criteria_spec.rb +2 -0
  54. data/spec/mongoid/encryptable_spec.rb +115 -0
  55. data/spec/mongoid/matcher/regexp_budget_spec.rb +576 -0
  56. data/spec/mongoid/search_indexable_spec.rb +34 -1
  57. data/spec/mongoid/tasks/database_spec.rb +31 -0
  58. data/spec/support/crypt/models.rb +253 -0
  59. data/spec/support/models/modifier_models.rb +32 -0
  60. metadata +17 -2
@@ -8,11 +8,38 @@ require_relative '../association/referenced/has_and_belongs_to_many_models'
8
8
  require_relative './nested_spec_models'
9
9
 
10
10
  describe Mongoid::Attributes::Nested do
11
+ # Expects the enclosing context to define `build_with_id`, taking an id
12
+ # value and returning the document built with it in nested attributes.
13
+ shared_examples 'rejects non-scalar ids' do |klass_name|
14
+ not_found = /not found for class #{klass_name}/
15
+
16
+ context 'when the id is an operator hash' do
17
+ it 'raises a document not found error' do
18
+ expect { build_with_id({ '$ne' => nil }) }
19
+ .to raise_error(Mongoid::Errors::DocumentNotFound, not_found)
20
+ end
21
+ end
22
+
23
+ context 'when the id is an array' do
24
+ it 'raises a document not found error' do
25
+ expect { build_with_id([ BSON::ObjectId.new ]) }
26
+ .to raise_error(Mongoid::Errors::DocumentNotFound, not_found)
27
+ end
28
+ end
29
+
30
+ context 'when the id is a malformed object id hash' do
31
+ it 'raises a document not found error' do
32
+ expect { build_with_id({ '$oid' => 'junk' }) }
33
+ .to raise_error(Mongoid::Errors::DocumentNotFound, not_found)
34
+ end
35
+ end
36
+ end
11
37
 
12
38
  describe ".accepts_nested_attributes_for" do
13
39
 
14
40
  context "when the autosave option is not defined" do
15
41
 
42
+
16
43
  let(:person) do
17
44
  Person.new
18
45
  end
@@ -202,6 +229,30 @@ describe Mongoid::Attributes::Nested do
202
229
  end
203
230
  end
204
231
  end
232
+
233
+ context 'when the id in the attributes is not a scalar' do
234
+ let(:person) { Person.create! }
235
+
236
+ def build_with_id(id)
237
+ person.update!(posts_attributes: { '0' => { id: id, title: 'Crafted' } })
238
+ end
239
+
240
+ context 'when the association is empty' do
241
+ include_examples 'rejects non-scalar ids', 'Post'
242
+ end
243
+
244
+ context 'when the association is not empty' do
245
+ before { person.posts.create!(title: 'Existing') }
246
+
247
+ include_examples 'rejects non-scalar ids', 'Post'
248
+ end
249
+
250
+ context 'when allow_reparenting_via_nested_attributes is true' do
251
+ config_override :allow_reparenting_via_nested_attributes, true
252
+
253
+ include_examples 'rejects non-scalar ids', 'Post'
254
+ end
255
+ end
205
256
  end
206
257
 
207
258
  context 'when the relation is a has-and-belongs-to-many' do
@@ -226,16 +277,100 @@ describe Mongoid::Attributes::Nested do
226
277
  )
227
278
  end
228
279
 
229
- it "sets the nested attributes" do
230
- expect(person.preferences.map(&:name)).to eq([preference.name])
280
+ context 'when allow_reparenting_via_nested_attributes is false' do
281
+ config_override :allow_reparenting_via_nested_attributes, false
282
+
283
+ it 'raises a document not found error' do
284
+ expect { person }.to raise_error(Mongoid::Errors::DocumentNotFound,
285
+ /Document\(s\) not found for class Preference/)
286
+ end
231
287
  end
232
288
 
233
- it "updates attributes of existing document which is added to relation" do
234
- preference_name = 'updated preference'
235
- person = Person.new(
236
- preferences_attributes: { 0 => { id: preference.id, name: preference_name } }
237
- )
238
- expect(person.preferences.map(&:name)).to eq([preference_name])
289
+ context 'when allow_reparenting_via_nested_attributes is true' do
290
+ config_override :allow_reparenting_via_nested_attributes, true
291
+
292
+ it 'sets the nested attributes' do
293
+ expect(person.preferences.map(&:name)).to eq([ preference.name ])
294
+ end
295
+
296
+ it 'updates attributes of existing document which is added to relation' do
297
+ preference_name = 'updated preference'
298
+ person = Person.new(
299
+ preferences_attributes: { 0 => { id: preference.id, name: preference_name } }
300
+ )
301
+ expect(person.preferences.map(&:name)).to eq([ preference_name ])
302
+ end
303
+
304
+ context 'when the id does not correspond to an existing document' do
305
+ let(:person) do
306
+ Person.new(
307
+ preferences_attributes: { 0 => { id: BSON::ObjectId.new, name: 'Ghost' } }
308
+ )
309
+ end
310
+
311
+ context 'when raise_not_found_error is true' do
312
+ config_override :raise_not_found_error, true
313
+
314
+ it 'raises a document not found error' do
315
+ expect { person }.to raise_error(Mongoid::Errors::DocumentNotFound,
316
+ /Document\(s\) not found for class Preference/)
317
+ end
318
+ end
319
+
320
+ context 'when raise_not_found_error is false' do
321
+ config_override :raise_not_found_error, false
322
+
323
+ it 'raises a document not found error' do
324
+ expect { person }.to raise_error(Mongoid::Errors::DocumentNotFound,
325
+ /Document\(s\) not found for class Preference/)
326
+ end
327
+ end
328
+ end
329
+ end
330
+
331
+ context 'when _destroy is true for a document not in the relation' do
332
+ before do
333
+ Person.send(:undef_method, :preferences_attributes=)
334
+ Person.accepts_nested_attributes_for :preferences, allow_destroy: true
335
+ end
336
+
337
+ let(:person) do
338
+ Person.new(
339
+ preferences_attributes: { 0 => { id: preference.id, _destroy: '1' } }
340
+ )
341
+ end
342
+
343
+ it 'does not raise UnknownAttribute' do
344
+ expect { person }.not_to raise_error
345
+ end
346
+
347
+ it 'does not add the document to the relation' do
348
+ expect(person.preferences).to be_empty
349
+ end
350
+ end
351
+ end
352
+
353
+ context 'when the id in the attributes is not a scalar' do
354
+ let(:target) { Person.create! }
355
+
356
+ def build_with_id(id)
357
+ target.update!(preferences_attributes: { 0 => { id: id, name: 'Crafted' } })
358
+ end
359
+
360
+ context 'when the association is empty' do
361
+ include_examples 'rejects non-scalar ids', 'Preference'
362
+ end
363
+
364
+ context 'when the association is not empty' do
365
+ before { target.preferences.create!(name: 'Existing') }
366
+
367
+ include_examples 'rejects non-scalar ids', 'Preference'
368
+ end
369
+
370
+ context 'when allow_reparenting_via_nested_attributes is true' do
371
+ config_override :allow_reparenting_via_nested_attributes, true
372
+
373
+ include_examples 'rejects non-scalar ids', 'Preference'
239
374
  end
240
375
  end
241
376
  end
@@ -3041,7 +3176,7 @@ describe Mongoid::Attributes::Nested do
3041
3176
  { '0' =>
3042
3177
  { 'id' => BSON::ObjectId.new.to_s, 'title' => 'Rogue' } }
3043
3178
  end.to raise_error(Mongoid::Errors::DocumentNotFound,
3044
- /Document\(s\) not found for class Post/)
3179
+ /Document not found for class Post/)
3045
3180
  end
3046
3181
  end
3047
3182
  end
@@ -3076,7 +3211,7 @@ describe Mongoid::Attributes::Nested do
3076
3211
  person.posts_attributes =
3077
3212
  { 'foo' => { 'id' => 'test', 'title' => 'Test' } }
3078
3213
  end.to raise_error(Mongoid::Errors::DocumentNotFound,
3079
- /Document\(s\) not found for class Post/)
3214
+ /Document not found for class Post/)
3080
3215
  end
3081
3216
  end
3082
3217
  end
@@ -5025,4 +5160,63 @@ describe Mongoid::Attributes::Nested do
5025
5160
  end
5026
5161
  end
5027
5162
  end
5163
+
5164
+ context 'when an id in nested attributes belongs to another parent' do
5165
+ let(:victim) { Person.create! }
5166
+ let(:attacker) { Person.create! }
5167
+
5168
+ context 'when the association is a has-many' do
5169
+ before do
5170
+ Person.send(:undef_method, :posts_attributes=)
5171
+ Person.accepts_nested_attributes_for :posts
5172
+ end
5173
+
5174
+ let!(:victim_post) { victim.posts.create!(title: 'Private') }
5175
+
5176
+ it 'does not update the other parent\'s document' do
5177
+ expect { attacker.update!(posts_attributes: { '0' => { id: victim_post.id, title: 'Overwritten' } }) }
5178
+ .to raise_error(Mongoid::Errors::DocumentNotFound)
5179
+ expect(victim_post.reload.title).to eq('Private')
5180
+ end
5181
+
5182
+ it 'does not add the other parent\'s document to the association' do
5183
+ expect { attacker.update!(posts_attributes: { '0' => { id: victim_post.id, title: 'Overwritten' } }) }
5184
+ .to raise_error(Mongoid::Errors::DocumentNotFound)
5185
+ expect(attacker.reload.posts).to be_empty
5186
+ end
5187
+
5188
+ it 'does not update via a direct association assignment' do
5189
+ expect { attacker.update!(posts: { '0' => { id: victim_post.id, title: 'Overwritten' } }) }
5190
+ .to raise_error(Mongoid::Errors::DocumentNotFound)
5191
+ expect(victim_post.reload.title).to eq('Private')
5192
+ end
5193
+ end
5194
+
5195
+ context 'when the association is a has-and-belongs-to-many' do
5196
+ before do
5197
+ Person.send(:undef_method, :preferences_attributes=)
5198
+ Person.accepts_nested_attributes_for :preferences
5199
+ end
5200
+
5201
+ let!(:victim_preference) { victim.preferences.create!(name: 'Private') }
5202
+
5203
+ it 'does not update the other parent\'s document' do
5204
+ expect { attacker.update!(preferences_attributes: { '0' => { id: victim_preference.id, name: 'Overwritten' } }) }
5205
+ .to raise_error(Mongoid::Errors::DocumentNotFound)
5206
+ expect(victim_preference.reload.name).to eq('Private')
5207
+ end
5208
+
5209
+ it 'does not add the other parent\'s document to the association' do
5210
+ expect { attacker.update!(preferences_attributes: { '0' => { id: victim_preference.id, name: 'Overwritten' } }) }
5211
+ .to raise_error(Mongoid::Errors::DocumentNotFound)
5212
+ expect(attacker.reload.preferences).to be_empty
5213
+ end
5214
+
5215
+ it 'does not update via a direct association assignment' do
5216
+ expect { attacker.update!(preferences: { '0' => { id: victim_preference.id, name: 'Overwritten' } }) }
5217
+ .to raise_error(Mongoid::Errors::DocumentNotFound)
5218
+ expect(victim_preference.reload.name).to eq('Private')
5219
+ end
5220
+ end
5221
+ end
5028
5222
  end
@@ -354,6 +354,42 @@ describe Mongoid::Clients::Factory do
354
354
  end
355
355
  end
356
356
  end
357
+
358
+ context 'when bypass_auto_encryption is set' do
359
+ it 'warns that encryption is bypassed' do
360
+ expect(Mongoid::Warnings).to receive(:warn_bypass_auto_encryption)
361
+
362
+ described_class.send(
363
+ :build_auto_encryption_options,
364
+ { auto_encryption_options: { bypass_auto_encryption: true, schema_map: {} } },
365
+ database_id
366
+ )
367
+ end
368
+ end
369
+
370
+ context 'when bypass_auto_encryption is not set' do
371
+ it 'does not warn' do
372
+ expect(Mongoid::Warnings).not_to receive(:warn_bypass_auto_encryption)
373
+
374
+ described_class.send(
375
+ :build_auto_encryption_options,
376
+ { auto_encryption_options: { schema_map: {} } },
377
+ database_id
378
+ )
379
+ end
380
+ end
381
+
382
+ context 'when bypass_auto_encryption is false' do
383
+ it 'does not warn' do
384
+ expect(Mongoid::Warnings).not_to receive(:warn_bypass_auto_encryption)
385
+
386
+ described_class.send(
387
+ :build_auto_encryption_options,
388
+ { auto_encryption_options: { bypass_auto_encryption: false, schema_map: {} } },
389
+ database_id
390
+ )
391
+ end
392
+ end
357
393
  end
358
394
 
359
395
  context "when no name is provided" do
@@ -41,6 +41,18 @@ describe Mongoid::Config::Defaults do
41
41
  end
42
42
  end
43
43
 
44
+ shared_examples 'uses settings for 9.0' do
45
+ it 'uses settings for 9.0' do
46
+ expect(Mongoid.autosave_saves_unchanged_documents).to be true
47
+ end
48
+ end
49
+
50
+ shared_examples 'never allows reparenting via nested attributes' do
51
+ it 'leaves allow_reparenting_via_nested_attributes false' do
52
+ expect(Mongoid.allow_reparenting_via_nested_attributes).to be false
53
+ end
54
+ end
55
+
44
56
  context "when giving a valid version" do
45
57
 
46
58
  before do
@@ -57,6 +69,8 @@ describe Mongoid::Config::Defaults do
57
69
 
58
70
  it_behaves_like "uses settings for 8.0"
59
71
  it_behaves_like "uses settings for 8.1"
72
+ it_behaves_like 'uses settings for 9.0'
73
+ it_behaves_like 'never allows reparenting via nested attributes'
60
74
  end
61
75
 
62
76
  context "when the given version is 8.1" do
@@ -65,6 +79,8 @@ describe Mongoid::Config::Defaults do
65
79
 
66
80
  it_behaves_like "does not use settings for 8.0"
67
81
  it_behaves_like "uses settings for 8.1"
82
+ it_behaves_like 'uses settings for 9.0'
83
+ it_behaves_like 'never allows reparenting via nested attributes'
68
84
  end
69
85
 
70
86
  context "when the given version is 9.0" do
@@ -73,6 +89,8 @@ describe Mongoid::Config::Defaults do
73
89
 
74
90
  it_behaves_like "does not use settings for 8.0"
75
91
  it_behaves_like "does not use settings for 8.1"
92
+ it_behaves_like 'uses settings for 9.0'
93
+ it_behaves_like 'never allows reparenting via nested attributes'
76
94
  end
77
95
  end
78
96