mongoid 9.1.0 → 9.1.1

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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/lib/config/locales/en.yml +49 -0
  3. data/lib/mongoid/association/accessors.rb +9 -1
  4. data/lib/mongoid/association/depending.rb +14 -10
  5. data/lib/mongoid/association/eager.rb +9 -0
  6. data/lib/mongoid/association/eager_load/discriminated_inclusion.rb +88 -0
  7. data/lib/mongoid/association/eager_load/embedded_distributor.rb +151 -0
  8. data/lib/mongoid/association/eager_load/inclusion.rb +173 -0
  9. data/lib/mongoid/association/eager_load/inclusion_tree.rb +96 -0
  10. data/lib/mongoid/association/eager_load/lookup_pipeline.rb +121 -0
  11. data/lib/mongoid/association/eager_load/polymorphic_preloader.rb +61 -0
  12. data/lib/mongoid/association/eager_load/polymorphic_targets.rb +151 -0
  13. data/lib/mongoid/association/eager_loadable.rb +57 -100
  14. data/lib/mongoid/association/embedded/embeds_many/proxy.rb +6 -1
  15. data/lib/mongoid/association/embedded/embeds_one.rb +1 -0
  16. data/lib/mongoid/association/macros.rb +13 -1
  17. data/lib/mongoid/association/nested/many.rb +34 -5
  18. data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
  19. data/lib/mongoid/association/nested/one.rb +6 -1
  20. data/lib/mongoid/association/options.rb +17 -0
  21. data/lib/mongoid/association/referenced/belongs_to/eager.rb +6 -1
  22. data/lib/mongoid/association/referenced/belongs_to.rb +1 -0
  23. data/lib/mongoid/association/referenced/counter_cache.rb +3 -3
  24. data/lib/mongoid/association/referenced/has_and_belongs_to_many.rb +5 -0
  25. data/lib/mongoid/association/referenced/has_many/enumerable.rb +3 -1
  26. data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
  27. data/lib/mongoid/association/referenced/has_many_through/eager.rb +99 -0
  28. data/lib/mongoid/association/referenced/has_many_through/proxy.rb +68 -0
  29. data/lib/mongoid/association/referenced/has_many_through.rb +201 -0
  30. data/lib/mongoid/association/referenced/has_one.rb +1 -0
  31. data/lib/mongoid/association/referenced/has_one_through/eager.rb +65 -0
  32. data/lib/mongoid/association/referenced/has_one_through/proxy.rb +30 -0
  33. data/lib/mongoid/association/referenced/has_one_through.rb +167 -0
  34. data/lib/mongoid/association/referenced.rb +2 -0
  35. data/lib/mongoid/association/relatable.rb +40 -0
  36. data/lib/mongoid/association.rb +7 -0
  37. data/lib/mongoid/clients/factory.rb +7 -13
  38. data/lib/mongoid/collection_configurable.rb +8 -0
  39. data/lib/mongoid/config/defaults.rb +0 -1
  40. data/lib/mongoid/config/encryption.rb +36 -19
  41. data/lib/mongoid/config.rb +56 -5
  42. data/lib/mongoid/contextual/aggregable/memory.rb +6 -2
  43. data/lib/mongoid/contextual/memory.rb +18 -7
  44. data/lib/mongoid/criteria/includable.rb +35 -11
  45. data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
  46. data/lib/mongoid/criteria/queryable/selectable.rb +123 -7
  47. data/lib/mongoid/deprecable.rb +45 -2
  48. data/lib/mongoid/encryptable.rb +46 -0
  49. data/lib/mongoid/errors/config_redactor.rb +43 -0
  50. data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
  51. data/lib/mongoid/errors/mixed_client_configuration.rb +1 -1
  52. data/lib/mongoid/errors/no_client_database.rb +1 -1
  53. data/lib/mongoid/errors/no_client_hosts.rb +1 -1
  54. data/lib/mongoid/errors/no_encryption_schema.rb +28 -0
  55. data/lib/mongoid/errors/readonly_association.rb +29 -0
  56. data/lib/mongoid/errors.rb +4 -0
  57. data/lib/mongoid/extensions/vector.rb +64 -0
  58. data/lib/mongoid/extensions.rb +1 -0
  59. data/lib/mongoid/field_readable.rb +69 -0
  60. data/lib/mongoid/indexable.rb +38 -26
  61. data/lib/mongoid/matchable.rb +6 -1
  62. data/lib/mongoid/matcher/eq_impl_with_regexp.rb +2 -4
  63. data/lib/mongoid/matcher/regex.rb +11 -12
  64. data/lib/mongoid/matcher/regexp_budget.rb +372 -0
  65. data/lib/mongoid/matcher.rb +1 -0
  66. data/lib/mongoid/persistable/creatable.rb +1 -1
  67. data/lib/mongoid/persistable/updatable.rb +9 -11
  68. data/lib/mongoid/persistence_context.rb +35 -0
  69. data/lib/mongoid/search_indexable.rb +64 -23
  70. data/lib/mongoid/serializable.rb +12 -4
  71. data/lib/mongoid/tasks/database.rb +23 -9
  72. data/lib/mongoid/threaded.rb +36 -0
  73. data/lib/mongoid/timestamps/timeless.rb +84 -10
  74. data/lib/mongoid/traversable.rb +2 -2
  75. data/lib/mongoid/validatable/associated.rb +18 -2
  76. data/lib/mongoid/version.rb +1 -1
  77. data/spec/integration/app_spec.rb +7 -1
  78. data/spec/integration/associations/belongs_to_spec.rb +13 -0
  79. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
  80. data/spec/integration/dots_and_dollars_spec.rb +12 -2
  81. data/spec/integration/encryption_spec.rb +151 -0
  82. data/spec/integration/matcher_operator_data/regex.yml +21 -0
  83. data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
  84. data/spec/integration/query_operator_guard_spec.rb +89 -0
  85. data/spec/mongoid/association/eager_loadable_spec.rb +47 -0
  86. data/spec/mongoid/association/eager_spec.rb +2 -2
  87. data/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +33 -0
  88. data/spec/mongoid/association/fallback_spec.rb +425 -0
  89. data/spec/mongoid/association/macros_spec.rb +26 -0
  90. data/spec/mongoid/association/referenced/belongs_to/eager_spec.rb +17 -0
  91. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
  92. data/spec/mongoid/association/referenced/has_and_belongs_to_many_spec.rb +50 -0
  93. data/spec/mongoid/association/referenced/has_many/eager_spec.rb +33 -0
  94. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
  95. data/spec/mongoid/association/referenced/has_many_through/eager_spec.rb +58 -0
  96. data/spec/mongoid/association/referenced/has_many_through/proxy_spec.rb +62 -0
  97. data/spec/mongoid/association/referenced/has_many_through_spec.rb +399 -0
  98. data/spec/mongoid/association/referenced/has_one_through/eager_spec.rb +58 -0
  99. data/spec/mongoid/association/referenced/has_one_through/proxy_spec.rb +49 -0
  100. data/spec/mongoid/association/referenced/has_one_through_spec.rb +211 -0
  101. data/spec/mongoid/attributes/nested_spec.rb +249 -8
  102. data/spec/mongoid/config/defaults_spec.rb +33 -0
  103. data/spec/mongoid/config/encryption_spec.rb +228 -0
  104. data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
  105. data/spec/mongoid/contextual/memory_spec.rb +177 -0
  106. data/spec/mongoid/criteria/includable_spec.rb +875 -0
  107. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +15 -1
  108. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +201 -0
  109. data/spec/mongoid/criteria_spec.rb +3 -0
  110. data/spec/mongoid/encryptable_spec.rb +61 -0
  111. data/spec/mongoid/errors/config_redactor_spec.rb +112 -0
  112. data/spec/mongoid/errors/mixed_client_configuration_spec.rb +31 -0
  113. data/spec/mongoid/errors/no_client_database_spec.rb +29 -0
  114. data/spec/mongoid/errors/no_client_hosts_spec.rb +26 -0
  115. data/spec/mongoid/errors/readonly_association_spec.rb +34 -0
  116. data/spec/mongoid/extensions/vector_spec.rb +175 -0
  117. data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
  118. data/spec/mongoid/search_indexable_spec.rb +295 -9
  119. data/spec/mongoid/tasks/database_spec.rb +18 -0
  120. data/spec/mongoid/timestamps/timeless_spec.rb +197 -5
  121. data/spec/mongoid/touchable_spec.rb +25 -0
  122. data/spec/mongoid/touchable_spec_models.rb +10 -0
  123. data/spec/mongoid/validatable/associated_spec.rb +19 -0
  124. data/spec/spec_helper.rb +5 -1
  125. data/spec/support/crypt/models.rb +157 -0
  126. metadata +52 -2
@@ -72,6 +72,19 @@ describe Mongoid::Config::Encryption do
72
72
  end
73
73
  end
74
74
 
75
+ # Models are registered in class load order, and Rails eager loads
76
+ # app/models alphabetically, so a child whose file name sorts before
77
+ # its parent's arrives first.
78
+ context 'when the embedded model comes before its parent' do
79
+ let(:models) do
80
+ [ Crypt::Insurance, Crypt::Patient ]
81
+ end
82
+
83
+ it 'returns a map of encryption schemas' do
84
+ expect(encryption_schema_map).to eq(expected_schema_map)
85
+ end
86
+ end
87
+
75
88
  context 'and fields do not have encryption options' do
76
89
  let(:models) do
77
90
  [ Crypt::Car ]
@@ -106,6 +119,7 @@ describe Mongoid::Config::Encryption do
106
119
  let(:expected_schema_map) do
107
120
  {
108
121
  'mongoid_test.crypt_users' => {
122
+ 'bsonType' => 'object',
109
123
  'properties' => {
110
124
  'name' => {
111
125
  'encrypt' => {
@@ -145,5 +159,219 @@ describe Mongoid::Config::Encryption do
145
159
  expect(encryption_schema_map).to eq({})
146
160
  end
147
161
  end
162
+
163
+ # A field left out of the schema map is written in plaintext, with no
164
+ # exception and nothing logged, so an omission here is silent data
165
+ # exposure rather than a broken feature.
166
+ context 'when the encrypted fields are on an embedded model' do
167
+ let(:token_properties) do
168
+ {
169
+ 'bsonType' => 'object',
170
+ 'properties' => {
171
+ 'value' => {
172
+ 'encrypt' => {
173
+ 'bsonType' => 'string',
174
+ 'algorithm' => 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
175
+ }
176
+ }
177
+ }
178
+ }
179
+ end
180
+
181
+ context 'when two models embed the same encrypted model' do
182
+ let(:models) do
183
+ [ Crypt::Vault, Crypt::Chest ]
184
+ end
185
+
186
+ it 'maps the embedded model for the first parent' do
187
+ expect(encryption_schema_map.dig('mongoid_test.crypt_vaults', 'properties', 'token'))
188
+ .to eq(token_properties)
189
+ end
190
+
191
+ it 'maps the embedded model for the second parent' do
192
+ expect(encryption_schema_map.dig('mongoid_test.crypt_chests', 'properties', 'token'))
193
+ .to eq(token_properties)
194
+ end
195
+ end
196
+
197
+ context 'when one model embeds the same encrypted model twice' do
198
+ let(:models) do
199
+ [ Crypt::Ledger ]
200
+ end
201
+
202
+ it 'maps the first relation' do
203
+ expect(encryption_schema_map.dig('mongoid_test.crypt_ledgers', 'properties', 'primary_token'))
204
+ .to eq(token_properties)
205
+ end
206
+
207
+ it 'maps the second relation' do
208
+ expect(encryption_schema_map.dig('mongoid_test.crypt_ledgers', 'properties', 'backup_token'))
209
+ .to eq(token_properties)
210
+ end
211
+ end
212
+
213
+ # relation_class constantizes, and a polymorphic embedded_in has no class
214
+ # to resolve, so the walk has to look at the relation type first.
215
+ context 'when the embedded model is embedded polymorphically' do
216
+ let(:models) do
217
+ [ Crypt::Vault ]
218
+ end
219
+
220
+ it 'does not raise' do
221
+ expect { encryption_schema_map }.not_to raise_error
222
+ end
223
+ end
224
+
225
+ context 'when the embedded model comes before its parent' do
226
+ let(:models) do
227
+ [ Crypt::Token, Crypt::Vault ]
228
+ end
229
+
230
+ it 'maps the embedded model' do
231
+ expect(encryption_schema_map.dig('mongoid_test.crypt_vaults', 'properties', 'token'))
232
+ .to eq(token_properties)
233
+ end
234
+ end
235
+
236
+ context 'when the parent has no encrypted field of its own' do
237
+ let(:models) do
238
+ [ Crypt::Wallet, Crypt::Token ]
239
+ end
240
+
241
+ it 'maps the embedded model' do
242
+ expect(encryption_schema_map.dig('mongoid_test.crypt_wallets', 'properties', 'token'))
243
+ .to eq(token_properties)
244
+ end
245
+ end
246
+
247
+ context 'when the encrypted model is nested three levels deep' do
248
+ let(:account_properties) do
249
+ {
250
+ 'bsonType' => 'object',
251
+ 'properties' => {
252
+ 'credential' => {
253
+ 'bsonType' => 'object',
254
+ 'properties' => {
255
+ 'secret' => {
256
+ 'encrypt' => {
257
+ 'bsonType' => 'string',
258
+ 'algorithm' => 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
259
+ }
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
265
+ end
266
+
267
+ context 'when the models are listed outermost first' do
268
+ let(:models) do
269
+ [ Crypt::Owner, Crypt::Account, Crypt::Credential ]
270
+ end
271
+
272
+ it 'maps the whole subtree' do
273
+ expect(encryption_schema_map.dig('mongoid_test.crypt_owners', 'properties', 'account'))
274
+ .to eq(account_properties)
275
+ end
276
+ end
277
+
278
+ context 'when the middle model comes first' do
279
+ let(:models) do
280
+ [ Crypt::Account, Crypt::Owner, Crypt::Credential ]
281
+ end
282
+
283
+ it 'maps the whole subtree' do
284
+ expect(encryption_schema_map.dig('mongoid_test.crypt_owners', 'properties', 'account'))
285
+ .to eq(account_properties)
286
+ end
287
+ end
288
+ end
289
+
290
+ # The walk has to keep stopping at a model it is already inside of,
291
+ # otherwise a self-embedding model recurses forever.
292
+ context 'when a model embeds itself' do
293
+ let(:models) do
294
+ [ Crypt::Article ]
295
+ end
296
+
297
+ it 'terminates' do
298
+ expect { encryption_schema_map }.not_to raise_error
299
+ end
300
+
301
+ it 'maps the embedded model without descending into the cycle' do
302
+ expect(encryption_schema_map.dig('mongoid_test.crypt_articles', 'properties', 'comment')).to eq(
303
+ 'bsonType' => 'object',
304
+ 'properties' => {
305
+ 'body' => {
306
+ 'encrypt' => {
307
+ 'bsonType' => 'string',
308
+ 'algorithm' => 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
309
+ }
310
+ }
311
+ }
312
+ )
313
+ end
314
+ end
315
+ end
316
+
317
+ context 'when an encrypted model has a callable database name' do
318
+ let(:models) do
319
+ [ Crypt::DynamicCar ]
320
+ end
321
+
322
+ it 'does not build a key from the unresolved callable' do
323
+ expect(encryption_schema_map.keys.grep(/Proc/)).to be_empty
324
+ end
325
+
326
+ it 'does not build a key that the model can never write to' do
327
+ expect(encryption_schema_map.keys).to all(satisfy { |key| key.count('.') == 1 })
328
+ end
329
+ end
330
+
331
+ context 'when an encrypted model has a callable database name that needs a tenant' do
332
+ let(:models) do
333
+ [ Crypt::TenantCar ]
334
+ end
335
+
336
+ # Resolving the callable while the map is built is not a fix: the
337
+ # documented multi-tenant idiom has no correct value at client
338
+ # construction time. Generating the map must not depend on it.
339
+ it 'does not raise' do
340
+ expect { encryption_schema_map }.not_to raise_error
341
+ end
342
+
343
+ it 'does not pin the map to whichever tenant happens to be current' do
344
+ Thread.current[:tenant_database] = 'tenant_a'
345
+ expect(encryption_schema_map.keys).not_to include('tenant_a.crypt_tenant_cars')
346
+ ensure
347
+ Thread.current[:tenant_database] = nil
348
+ end
349
+ end
350
+
351
+ # The map is generated over every model in the application, and a model may
352
+ # name an embedded class that is never defined. Resolving that name raises,
353
+ # which would take down every client build.
354
+ context 'when a model names an embedded class that is not defined' do
355
+ let(:models) do
356
+ [ Crypt::Drawer, Crypt::Cabinet ]
357
+ end
358
+
359
+ it 'does not raise' do
360
+ expect { encryption_schema_map }.not_to raise_error
361
+ end
362
+
363
+ it 'leaves out the model with nothing to encrypt' do
364
+ expect(encryption_schema_map.keys).not_to include('mongoid_test.crypt_drawers')
365
+ end
366
+
367
+ it 'still maps the encrypted fields of the model itself' do
368
+ expect(encryption_schema_map.dig('mongoid_test.crypt_cabinets', 'properties', 'label')).to eq(
369
+ 'encrypt' => {
370
+ 'bsonType' => 'string',
371
+ 'algorithm' => 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
372
+ }
373
+ )
374
+ end
375
+ end
148
376
  end
149
377
  end
@@ -545,4 +545,95 @@ describe Mongoid::Contextual::Aggregable::Memory do
545
545
  end
546
546
  end
547
547
  end
548
+
549
+ context 'when the field name is a method name' do
550
+ let!(:depeche) do
551
+ Band.create!(name: 'Depeche Mode', likes: 1000)
552
+ end
553
+
554
+ let(:criteria) do
555
+ Band.all.tap do |crit|
556
+ crit.documents = [ depeche ]
557
+ end
558
+ end
559
+
560
+ it 'does not call the method for sum' do
561
+ expect(depeche).not_to receive(:delete)
562
+ expect(context.sum(:delete)).to eq(0)
563
+ end
564
+
565
+ it 'does not call the method for min' do
566
+ expect(depeche).not_to receive(:delete)
567
+ expect(context.min(:delete)).to be_nil
568
+ end
569
+
570
+ it 'does not call the method for max' do
571
+ expect(depeche).not_to receive(:delete)
572
+ expect(context.max(:delete)).to be_nil
573
+ end
574
+
575
+ it 'does not call the method for avg' do
576
+ expect(depeche).not_to receive(:delete)
577
+ expect(context.avg(:delete)).to be_nil
578
+ end
579
+
580
+ it 'does not call the method for aggregates' do
581
+ expect(depeche).not_to receive(:delete)
582
+ expect(context.aggregates(:delete))
583
+ .to eq('count' => 0, 'avg' => nil, 'max' => nil, 'min' => nil, 'sum' => 0)
584
+ end
585
+
586
+ it 'does not call a private method for avg' do
587
+ expect(depeche).not_to receive(:exit)
588
+ expect(context.avg(:exit)).to be_nil
589
+ end
590
+ end
591
+
592
+ context 'when the field is not defined' do
593
+ let!(:depeche) do
594
+ Band.create!(name: 'Depeche Mode', likes: 1000)
595
+ end
596
+
597
+ let(:criteria) do
598
+ Band.all.tap do |crit|
599
+ crit.documents = [ depeche ]
600
+ end
601
+ end
602
+
603
+ it 'returns zero for sum' do
604
+ expect(context.sum(:not_a_field)).to eq(0)
605
+ end
606
+
607
+ it 'returns nil for min' do
608
+ expect(context.min(:not_a_field)).to be_nil
609
+ end
610
+
611
+ it 'returns nil for max' do
612
+ expect(context.max(:not_a_field)).to be_nil
613
+ end
614
+
615
+ it 'returns nil for avg' do
616
+ expect(context.avg(:not_a_field)).to be_nil
617
+ end
618
+ end
619
+
620
+ context 'when the field is the foreign key of a many to many association' do
621
+ let!(:preference) do
622
+ Preference.create!(name: 'nature')
623
+ end
624
+
625
+ let!(:person) do
626
+ Person.create!(preferences: [ preference ])
627
+ end
628
+
629
+ let(:criteria) do
630
+ Person.all.tap do |crit|
631
+ crit.documents = [ person ]
632
+ end
633
+ end
634
+
635
+ it 'aggregates the stored ids' do
636
+ expect(context.max(:preference_ids)).to eq([ preference.id ])
637
+ end
638
+ end
548
639
  end
@@ -556,6 +556,27 @@ describe Mongoid::Contextual::Memory do
556
556
  expect(context.distinct('label.sales')).to eq([ BigDecimal('1E2') ])
557
557
  end
558
558
  end
559
+
560
+ context 'when the field name is a method name' do
561
+ let!(:person) do
562
+ Person.create!(ssn: 'secret-ssn')
563
+ end
564
+
565
+ let!(:address) do
566
+ person.addresses.create!(street: 'hobrecht')
567
+ end
568
+
569
+ let(:criteria) do
570
+ Address.all.tap do |crit|
571
+ crit.documents = [ address ]
572
+ end
573
+ end
574
+
575
+ it 'does not call the method' do
576
+ expect(address).not_to receive(:destroy)
577
+ expect(context.distinct(:destroy)).to eq([ nil ])
578
+ end
579
+ end
559
580
  end
560
581
 
561
582
  describe '#each' do
@@ -1714,6 +1735,116 @@ describe Mongoid::Contextual::Memory do
1714
1735
  ])
1715
1736
  end
1716
1737
  end
1738
+
1739
+ context 'when plucking a dynamic attribute' do
1740
+ let(:criteria) do
1741
+ Band.all.tap do |crit|
1742
+ crit.documents = [ Band.create!(name: 'Depeche Mode', mood: 'dark') ]
1743
+ end
1744
+ end
1745
+
1746
+ it 'returns the value from the attributes' do
1747
+ expect(context.pluck(:mood)).to eq([ 'dark' ])
1748
+ end
1749
+ end
1750
+
1751
+ context 'when the field name is a method name' do
1752
+ let!(:person) do
1753
+ Person.create!(ssn: 'secret-ssn')
1754
+ end
1755
+
1756
+ let!(:address) do
1757
+ person.addresses.create!(street: 'hobrecht')
1758
+ end
1759
+
1760
+ let(:criteria) do
1761
+ Address.all.tap do |crit|
1762
+ crit.documents = [ address ]
1763
+ end
1764
+ end
1765
+
1766
+ it 'does not call the method' do
1767
+ expect(address).not_to receive(:destroy)
1768
+ expect(context.pluck(:destroy)).to eq([ nil ])
1769
+ end
1770
+
1771
+ it 'does not return the attributes hash' do
1772
+ expect(context.pluck(:attributes)).to eq([ nil ])
1773
+ end
1774
+
1775
+ it 'does not traverse to the parent document' do
1776
+ expect(context.pluck('_root.ssn')).to eq([ nil ])
1777
+ end
1778
+
1779
+ it 'does not destroy the parent document' do
1780
+ expect(context.pluck('_root.destroy')).to eq([ nil ])
1781
+ expect(Person.where(_id: person.id).count).to eq(1)
1782
+ end
1783
+ end
1784
+
1785
+ context 'when plucking a belongs_to association' do
1786
+ let!(:band) do
1787
+ Band.create!(name: 'Depeche Mode')
1788
+ end
1789
+
1790
+ let!(:artist) do
1791
+ Artist.create!(band: band)
1792
+ end
1793
+
1794
+ let(:criteria) do
1795
+ Artist.all.tap do |crit|
1796
+ crit.documents = [ artist ]
1797
+ end
1798
+ end
1799
+
1800
+ it 'traverses to the associated document' do
1801
+ expect(context.pluck('band.name')).to eq([ 'Depeche Mode' ])
1802
+ end
1803
+
1804
+ it 'returns the document for the association name' do
1805
+ expect(context.pluck(:band)).to eq([ band ])
1806
+ end
1807
+
1808
+ it 'returns the id for the foreign key' do
1809
+ expect(context.pluck(:band_id)).to eq([ band.id ])
1810
+ end
1811
+ end
1812
+
1813
+ context 'when plucking the foreign key of a many to many association' do
1814
+ let!(:preference) do
1815
+ Preference.create!(name: 'nature')
1816
+ end
1817
+
1818
+ let!(:person) do
1819
+ Person.create!(preferences: [ preference ])
1820
+ end
1821
+
1822
+ let(:criteria) do
1823
+ Person.all.tap do |crit|
1824
+ crit.documents = [ person ]
1825
+ end
1826
+ end
1827
+
1828
+ it 'returns the stored ids' do
1829
+ expect(context.pluck(:preference_ids)).to eq([ [ preference.id ] ])
1830
+ end
1831
+ end
1832
+
1833
+ context 'when the field path has an empty segment' do
1834
+ let!(:band) do
1835
+ Band.create!(name: 'Depeche Mode', label: Label.new(name: 'Mute'))
1836
+ end
1837
+
1838
+ let(:criteria) do
1839
+ Band.all.tap do |crit|
1840
+ crit.documents = [ band ]
1841
+ end
1842
+ end
1843
+
1844
+ it 'returns nil' do
1845
+ expect(context.pluck('label..name')).to eq([ nil ])
1846
+ end
1847
+ end
1717
1848
  end
1718
1849
 
1719
1850
  describe '#pick' do
@@ -1770,6 +1901,27 @@ describe Mongoid::Contextual::Memory do
1770
1901
  expect(picked).to be_nil
1771
1902
  end
1772
1903
  end
1904
+
1905
+ context 'when the field name is a method name' do
1906
+ let!(:person) do
1907
+ Person.create!(ssn: 'secret-ssn')
1908
+ end
1909
+
1910
+ let!(:address) do
1911
+ person.addresses.create!(street: 'hobrecht')
1912
+ end
1913
+
1914
+ let(:criteria) do
1915
+ Address.all.tap do |crit|
1916
+ crit.documents = [ address ]
1917
+ end
1918
+ end
1919
+
1920
+ it 'does not call the method' do
1921
+ expect(address).not_to receive(:destroy)
1922
+ expect(context.pick(:destroy)).to be_nil
1923
+ end
1924
+ end
1773
1925
  end
1774
1926
 
1775
1927
  describe '#tally' do
@@ -2234,6 +2386,31 @@ describe Mongoid::Contextual::Memory do
2234
2386
  )
2235
2387
  end
2236
2388
  end
2389
+
2390
+ context 'when the field name is a method name' do
2391
+ let!(:person) do
2392
+ Person.create!(ssn: 'secret-ssn')
2393
+ end
2394
+
2395
+ let!(:address) do
2396
+ person.addresses.create!(street: 'hobrecht')
2397
+ end
2398
+
2399
+ let(:criteria) do
2400
+ Address.all.tap do |crit|
2401
+ crit.documents = [ address ]
2402
+ end
2403
+ end
2404
+
2405
+ it 'does not call the method' do
2406
+ expect(address).not_to receive(:destroy)
2407
+ expect(context.tally(:destroy)).to eq(nil => 1)
2408
+ end
2409
+
2410
+ it 'does not disclose the parent document fields' do
2411
+ expect(context.tally('_root.ssn')).to eq(nil => 1)
2412
+ end
2413
+ end
2237
2414
  end
2238
2415
 
2239
2416
  describe '#inc' do