mongoid 9.0.11 → 9.0.12
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.
- checksums.yaml +4 -4
- data/lib/config/locales/en.yml +45 -0
- data/lib/mongoid/association/depending.rb +8 -4
- data/lib/mongoid/association/nested/many.rb +34 -5
- data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
- data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
- data/lib/mongoid/association/relatable.rb +17 -0
- data/lib/mongoid/collection_configurable.rb +8 -0
- data/lib/mongoid/config/encryption.rb +36 -18
- data/lib/mongoid/config.rb +56 -9
- data/lib/mongoid/contextual/aggregable/memory.rb +5 -2
- data/lib/mongoid/contextual/memory.rb +18 -7
- data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
- data/lib/mongoid/criteria/queryable/selectable.rb +109 -0
- data/lib/mongoid/encryptable.rb +46 -0
- data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
- data/lib/mongoid/errors/no_encryption_schema.rb +28 -0
- data/lib/mongoid/errors.rb +2 -0
- data/lib/mongoid/field_readable.rb +70 -0
- data/lib/mongoid/indexable.rb +39 -27
- data/lib/mongoid/matchable.rb +6 -1
- data/lib/mongoid/matcher/eq_impl_with_regexp.rb +4 -9
- data/lib/mongoid/matcher/regex.rb +9 -13
- data/lib/mongoid/matcher/regexp_budget.rb +383 -0
- data/lib/mongoid/matcher.rb +1 -0
- data/lib/mongoid/persistence_context.rb +35 -0
- data/lib/mongoid/search_indexable.rb +14 -7
- data/lib/mongoid/tasks/database.rb +23 -9
- data/lib/mongoid/threaded.rb +37 -0
- data/lib/mongoid/version.rb +1 -1
- data/spec/integration/app_spec.rb +7 -0
- data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
- data/spec/integration/dots_and_dollars_spec.rb +12 -2
- data/spec/integration/encryption_spec.rb +149 -0
- data/spec/integration/matcher_operator_data/regex.yml +21 -0
- data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
- data/spec/integration/query_operator_guard_spec.rb +89 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
- data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
- data/spec/mongoid/attributes/nested_spec.rb +204 -10
- data/spec/mongoid/config/defaults_spec.rb +18 -0
- data/spec/mongoid/config/encryption_spec.rb +228 -0
- data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
- data/spec/mongoid/contextual/memory_spec.rb +177 -0
- data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +2 -0
- data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +200 -0
- data/spec/mongoid/criteria_spec.rb +2 -0
- data/spec/mongoid/encryptable_spec.rb +61 -0
- data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
- data/spec/mongoid/tasks/database_spec.rb +18 -0
- data/spec/support/crypt/models.rb +157 -0
- metadata +14 -2
|
@@ -75,6 +75,19 @@ describe Mongoid::Config::Encryption do
|
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
# Models are registered in class load order, and Rails eager loads
|
|
79
|
+
# app/models alphabetically, so a child whose file name sorts before
|
|
80
|
+
# its parent's arrives first.
|
|
81
|
+
context 'when the embedded model comes before its parent' do
|
|
82
|
+
let(:models) do
|
|
83
|
+
[ Crypt::Insurance, Crypt::Patient ]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'returns a map of encryption schemas' do
|
|
87
|
+
expect(encryption_schema_map).to eq(expected_schema_map)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
78
91
|
context 'and fields do not have encryption options' do
|
|
79
92
|
let(:models) do
|
|
80
93
|
[Crypt::Car]
|
|
@@ -109,6 +122,7 @@ describe Mongoid::Config::Encryption do
|
|
|
109
122
|
let(:expected_schema_map) do
|
|
110
123
|
{
|
|
111
124
|
"mongoid_test.crypt_users" => {
|
|
125
|
+
'bsonType' => 'object',
|
|
112
126
|
"properties" => {
|
|
113
127
|
"name" => {
|
|
114
128
|
"encrypt" => {
|
|
@@ -148,5 +162,219 @@ describe Mongoid::Config::Encryption do
|
|
|
148
162
|
expect(encryption_schema_map).to eq({})
|
|
149
163
|
end
|
|
150
164
|
end
|
|
165
|
+
|
|
166
|
+
# A field left out of the schema map is written in plaintext, with no
|
|
167
|
+
# exception and nothing logged, so an omission here is silent data
|
|
168
|
+
# exposure rather than a broken feature.
|
|
169
|
+
context 'when the encrypted fields are on an embedded model' do
|
|
170
|
+
let(:token_properties) do
|
|
171
|
+
{
|
|
172
|
+
'bsonType' => 'object',
|
|
173
|
+
'properties' => {
|
|
174
|
+
'value' => {
|
|
175
|
+
'encrypt' => {
|
|
176
|
+
'bsonType' => 'string',
|
|
177
|
+
'algorithm' => 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
context 'when two models embed the same encrypted model' do
|
|
185
|
+
let(:models) do
|
|
186
|
+
[ Crypt::Vault, Crypt::Chest ]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it 'maps the embedded model for the first parent' do
|
|
190
|
+
expect(encryption_schema_map.dig('mongoid_test.crypt_vaults', 'properties', 'token'))
|
|
191
|
+
.to eq(token_properties)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it 'maps the embedded model for the second parent' do
|
|
195
|
+
expect(encryption_schema_map.dig('mongoid_test.crypt_chests', 'properties', 'token'))
|
|
196
|
+
.to eq(token_properties)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
context 'when one model embeds the same encrypted model twice' do
|
|
201
|
+
let(:models) do
|
|
202
|
+
[ Crypt::Ledger ]
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it 'maps the first relation' do
|
|
206
|
+
expect(encryption_schema_map.dig('mongoid_test.crypt_ledgers', 'properties', 'primary_token'))
|
|
207
|
+
.to eq(token_properties)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it 'maps the second relation' do
|
|
211
|
+
expect(encryption_schema_map.dig('mongoid_test.crypt_ledgers', 'properties', 'backup_token'))
|
|
212
|
+
.to eq(token_properties)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# relation_class constantizes, and a polymorphic embedded_in has no class
|
|
217
|
+
# to resolve, so the walk has to look at the relation type first.
|
|
218
|
+
context 'when the embedded model is embedded polymorphically' do
|
|
219
|
+
let(:models) do
|
|
220
|
+
[ Crypt::Vault ]
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it 'does not raise' do
|
|
224
|
+
expect { encryption_schema_map }.not_to raise_error
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
context 'when the embedded model comes before its parent' do
|
|
229
|
+
let(:models) do
|
|
230
|
+
[ Crypt::Token, Crypt::Vault ]
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it 'maps the embedded model' do
|
|
234
|
+
expect(encryption_schema_map.dig('mongoid_test.crypt_vaults', 'properties', 'token'))
|
|
235
|
+
.to eq(token_properties)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
context 'when the parent has no encrypted field of its own' do
|
|
240
|
+
let(:models) do
|
|
241
|
+
[ Crypt::Wallet, Crypt::Token ]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
it 'maps the embedded model' do
|
|
245
|
+
expect(encryption_schema_map.dig('mongoid_test.crypt_wallets', 'properties', 'token'))
|
|
246
|
+
.to eq(token_properties)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
context 'when the encrypted model is nested three levels deep' do
|
|
251
|
+
let(:account_properties) do
|
|
252
|
+
{
|
|
253
|
+
'bsonType' => 'object',
|
|
254
|
+
'properties' => {
|
|
255
|
+
'credential' => {
|
|
256
|
+
'bsonType' => 'object',
|
|
257
|
+
'properties' => {
|
|
258
|
+
'secret' => {
|
|
259
|
+
'encrypt' => {
|
|
260
|
+
'bsonType' => 'string',
|
|
261
|
+
'algorithm' => 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
context 'when the models are listed outermost first' do
|
|
271
|
+
let(:models) do
|
|
272
|
+
[ Crypt::Owner, Crypt::Account, Crypt::Credential ]
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
it 'maps the whole subtree' do
|
|
276
|
+
expect(encryption_schema_map.dig('mongoid_test.crypt_owners', 'properties', 'account'))
|
|
277
|
+
.to eq(account_properties)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
context 'when the middle model comes first' do
|
|
282
|
+
let(:models) do
|
|
283
|
+
[ Crypt::Account, Crypt::Owner, Crypt::Credential ]
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
it 'maps the whole subtree' do
|
|
287
|
+
expect(encryption_schema_map.dig('mongoid_test.crypt_owners', 'properties', 'account'))
|
|
288
|
+
.to eq(account_properties)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# The walk has to keep stopping at a model it is already inside of,
|
|
294
|
+
# otherwise a self-embedding model recurses forever.
|
|
295
|
+
context 'when a model embeds itself' do
|
|
296
|
+
let(:models) do
|
|
297
|
+
[ Crypt::Article ]
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
it 'terminates' do
|
|
301
|
+
expect { encryption_schema_map }.not_to raise_error
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
it 'maps the embedded model without descending into the cycle' do
|
|
305
|
+
expect(encryption_schema_map.dig('mongoid_test.crypt_articles', 'properties', 'comment')).to eq(
|
|
306
|
+
'bsonType' => 'object',
|
|
307
|
+
'properties' => {
|
|
308
|
+
'body' => {
|
|
309
|
+
'encrypt' => {
|
|
310
|
+
'bsonType' => 'string',
|
|
311
|
+
'algorithm' => 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
context 'when an encrypted model has a callable database name' do
|
|
321
|
+
let(:models) do
|
|
322
|
+
[ Crypt::DynamicCar ]
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
it 'does not build a key from the unresolved callable' do
|
|
326
|
+
expect(encryption_schema_map.keys.grep(/Proc/)).to be_empty
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
it 'does not build a key that the model can never write to' do
|
|
330
|
+
expect(encryption_schema_map.keys).to all(satisfy { |key| key.count('.') == 1 })
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
context 'when an encrypted model has a callable database name that needs a tenant' do
|
|
335
|
+
let(:models) do
|
|
336
|
+
[ Crypt::TenantCar ]
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Resolving the callable while the map is built is not a fix: the
|
|
340
|
+
# documented multi-tenant idiom has no correct value at client
|
|
341
|
+
# construction time. Generating the map must not depend on it.
|
|
342
|
+
it 'does not raise' do
|
|
343
|
+
expect { encryption_schema_map }.not_to raise_error
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
it 'does not pin the map to whichever tenant happens to be current' do
|
|
347
|
+
Thread.current[:tenant_database] = 'tenant_a'
|
|
348
|
+
expect(encryption_schema_map.keys).not_to include('tenant_a.crypt_tenant_cars')
|
|
349
|
+
ensure
|
|
350
|
+
Thread.current[:tenant_database] = nil
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# The map is generated over every model in the application, and a model may
|
|
355
|
+
# name an embedded class that is never defined. Resolving that name raises,
|
|
356
|
+
# which would take down every client build.
|
|
357
|
+
context 'when a model names an embedded class that is not defined' do
|
|
358
|
+
let(:models) do
|
|
359
|
+
[ Crypt::Drawer, Crypt::Cabinet ]
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
it 'does not raise' do
|
|
363
|
+
expect { encryption_schema_map }.not_to raise_error
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
it 'leaves out the model with nothing to encrypt' do
|
|
367
|
+
expect(encryption_schema_map.keys).not_to include('mongoid_test.crypt_drawers')
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
it 'still maps the encrypted fields of the model itself' do
|
|
371
|
+
expect(encryption_schema_map.dig('mongoid_test.crypt_cabinets', 'properties', 'label')).to eq(
|
|
372
|
+
'encrypt' => {
|
|
373
|
+
'bsonType' => 'string',
|
|
374
|
+
'algorithm' => 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
|
|
375
|
+
}
|
|
376
|
+
)
|
|
377
|
+
end
|
|
378
|
+
end
|
|
151
379
|
end
|
|
152
380
|
end
|
|
@@ -582,4 +582,95 @@ describe Mongoid::Contextual::Aggregable::Memory do
|
|
|
582
582
|
end
|
|
583
583
|
end
|
|
584
584
|
end
|
|
585
|
+
|
|
586
|
+
context 'when the field name is a method name' do
|
|
587
|
+
let!(:depeche) do
|
|
588
|
+
Band.create!(name: 'Depeche Mode', likes: 1000)
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
let(:criteria) do
|
|
592
|
+
Band.all.tap do |crit|
|
|
593
|
+
crit.documents = [ depeche ]
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
it 'does not call the method for sum' do
|
|
598
|
+
expect(depeche).not_to receive(:delete)
|
|
599
|
+
expect(context.sum(:delete)).to eq(0)
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
it 'does not call the method for min' do
|
|
603
|
+
expect(depeche).not_to receive(:delete)
|
|
604
|
+
expect(context.min(:delete)).to be_nil
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
it 'does not call the method for max' do
|
|
608
|
+
expect(depeche).not_to receive(:delete)
|
|
609
|
+
expect(context.max(:delete)).to be_nil
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
it 'does not call the method for avg' do
|
|
613
|
+
expect(depeche).not_to receive(:delete)
|
|
614
|
+
expect(context.avg(:delete)).to be_nil
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
it 'does not call the method for aggregates' do
|
|
618
|
+
expect(depeche).not_to receive(:delete)
|
|
619
|
+
expect(context.aggregates(:delete))
|
|
620
|
+
.to eq('count' => 0, 'avg' => nil, 'max' => nil, 'min' => nil, 'sum' => 0)
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
it 'does not call a private method for avg' do
|
|
624
|
+
expect(depeche).not_to receive(:exit)
|
|
625
|
+
expect(context.avg(:exit)).to be_nil
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
context 'when the field is not defined' do
|
|
630
|
+
let!(:depeche) do
|
|
631
|
+
Band.create!(name: 'Depeche Mode', likes: 1000)
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
let(:criteria) do
|
|
635
|
+
Band.all.tap do |crit|
|
|
636
|
+
crit.documents = [ depeche ]
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
it 'returns zero for sum' do
|
|
641
|
+
expect(context.sum(:not_a_field)).to eq(0)
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
it 'returns nil for min' do
|
|
645
|
+
expect(context.min(:not_a_field)).to be_nil
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
it 'returns nil for max' do
|
|
649
|
+
expect(context.max(:not_a_field)).to be_nil
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
it 'returns nil for avg' do
|
|
653
|
+
expect(context.avg(:not_a_field)).to be_nil
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
context 'when the field is the foreign key of a many to many association' do
|
|
658
|
+
let!(:preference) do
|
|
659
|
+
Preference.create!(name: 'nature')
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
let!(:person) do
|
|
663
|
+
Person.new(preferences: [ preference ])
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
let(:criteria) do
|
|
667
|
+
Person.all.tap do |crit|
|
|
668
|
+
crit.documents = [ person ]
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
it 'aggregates the stored ids' do
|
|
673
|
+
expect(context.max(:preference_ids)).to eq([ preference.id ])
|
|
674
|
+
end
|
|
675
|
+
end
|
|
585
676
|
end
|
|
@@ -588,6 +588,27 @@ describe Mongoid::Contextual::Memory do
|
|
|
588
588
|
expect(context.distinct("label.sales")).to eq([ BigDecimal("1E2") ])
|
|
589
589
|
end
|
|
590
590
|
end
|
|
591
|
+
|
|
592
|
+
context 'when the field name is a method name' do
|
|
593
|
+
let!(:person) do
|
|
594
|
+
Person.create!(ssn: 'secret-ssn')
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
let!(:address) do
|
|
598
|
+
person.addresses.create!(street: 'hobrecht')
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
let(:criteria) do
|
|
602
|
+
Address.all.tap do |crit|
|
|
603
|
+
crit.documents = [ address ]
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
it 'does not call the method' do
|
|
608
|
+
expect(address).not_to receive(:destroy)
|
|
609
|
+
expect(context.distinct(:destroy)).to eq([ nil ])
|
|
610
|
+
end
|
|
611
|
+
end
|
|
591
612
|
end
|
|
592
613
|
|
|
593
614
|
describe "#each" do
|
|
@@ -1826,6 +1847,116 @@ describe Mongoid::Contextual::Memory do
|
|
|
1826
1847
|
])
|
|
1827
1848
|
end
|
|
1828
1849
|
end
|
|
1850
|
+
|
|
1851
|
+
context 'when plucking a dynamic attribute' do
|
|
1852
|
+
let(:criteria) do
|
|
1853
|
+
Band.all.tap do |crit|
|
|
1854
|
+
crit.documents = [ Band.create!(name: 'Depeche Mode', mood: 'dark') ]
|
|
1855
|
+
end
|
|
1856
|
+
end
|
|
1857
|
+
|
|
1858
|
+
it 'returns the value from the attributes' do
|
|
1859
|
+
expect(context.pluck(:mood)).to eq([ 'dark' ])
|
|
1860
|
+
end
|
|
1861
|
+
end
|
|
1862
|
+
|
|
1863
|
+
context 'when the field name is a method name' do
|
|
1864
|
+
let!(:person) do
|
|
1865
|
+
Person.create!(ssn: 'secret-ssn')
|
|
1866
|
+
end
|
|
1867
|
+
|
|
1868
|
+
let!(:address) do
|
|
1869
|
+
person.addresses.create!(street: 'hobrecht')
|
|
1870
|
+
end
|
|
1871
|
+
|
|
1872
|
+
let(:criteria) do
|
|
1873
|
+
Address.all.tap do |crit|
|
|
1874
|
+
crit.documents = [ address ]
|
|
1875
|
+
end
|
|
1876
|
+
end
|
|
1877
|
+
|
|
1878
|
+
it 'does not call the method' do
|
|
1879
|
+
expect(address).not_to receive(:destroy)
|
|
1880
|
+
expect(context.pluck(:destroy)).to eq([ nil ])
|
|
1881
|
+
end
|
|
1882
|
+
|
|
1883
|
+
it 'does not return the attributes hash' do
|
|
1884
|
+
expect(context.pluck(:attributes)).to eq([ nil ])
|
|
1885
|
+
end
|
|
1886
|
+
|
|
1887
|
+
it 'does not traverse to the parent document' do
|
|
1888
|
+
expect(context.pluck('_root.ssn')).to eq([ nil ])
|
|
1889
|
+
end
|
|
1890
|
+
|
|
1891
|
+
it 'does not destroy the parent document' do
|
|
1892
|
+
expect(context.pluck('_root.destroy')).to eq([ nil ])
|
|
1893
|
+
expect(Person.where(_id: person.id).count).to eq(1)
|
|
1894
|
+
end
|
|
1895
|
+
end
|
|
1896
|
+
|
|
1897
|
+
context 'when plucking a belongs_to association' do
|
|
1898
|
+
let!(:band) do
|
|
1899
|
+
Band.create!(name: 'Depeche Mode')
|
|
1900
|
+
end
|
|
1901
|
+
|
|
1902
|
+
let!(:artist) do
|
|
1903
|
+
Artist.create!(band: band)
|
|
1904
|
+
end
|
|
1905
|
+
|
|
1906
|
+
let(:criteria) do
|
|
1907
|
+
Artist.all.tap do |crit|
|
|
1908
|
+
crit.documents = [ artist ]
|
|
1909
|
+
end
|
|
1910
|
+
end
|
|
1911
|
+
|
|
1912
|
+
it 'traverses to the associated document' do
|
|
1913
|
+
expect(context.pluck('band.name')).to eq([ 'Depeche Mode' ])
|
|
1914
|
+
end
|
|
1915
|
+
|
|
1916
|
+
it 'returns the document for the association name' do
|
|
1917
|
+
expect(context.pluck(:band)).to eq([ band ])
|
|
1918
|
+
end
|
|
1919
|
+
|
|
1920
|
+
it 'returns the id for the foreign key' do
|
|
1921
|
+
expect(context.pluck(:band_id)).to eq([ band.id ])
|
|
1922
|
+
end
|
|
1923
|
+
end
|
|
1924
|
+
|
|
1925
|
+
context 'when plucking the foreign key of a many to many association' do
|
|
1926
|
+
let!(:preference) do
|
|
1927
|
+
Preference.create!(name: 'nature')
|
|
1928
|
+
end
|
|
1929
|
+
|
|
1930
|
+
let!(:person) do
|
|
1931
|
+
Person.create!(preferences: [ preference ])
|
|
1932
|
+
end
|
|
1933
|
+
|
|
1934
|
+
let(:criteria) do
|
|
1935
|
+
Person.all.tap do |crit|
|
|
1936
|
+
crit.documents = [ person ]
|
|
1937
|
+
end
|
|
1938
|
+
end
|
|
1939
|
+
|
|
1940
|
+
it 'returns the stored ids' do
|
|
1941
|
+
expect(context.pluck(:preference_ids)).to eq([ [ preference.id ] ])
|
|
1942
|
+
end
|
|
1943
|
+
end
|
|
1944
|
+
|
|
1945
|
+
context 'when the field path has an empty segment' do
|
|
1946
|
+
let!(:band) do
|
|
1947
|
+
Band.create!(name: 'Depeche Mode', label: Label.new(name: 'Mute'))
|
|
1948
|
+
end
|
|
1949
|
+
|
|
1950
|
+
let(:criteria) do
|
|
1951
|
+
Band.all.tap do |crit|
|
|
1952
|
+
crit.documents = [ band ]
|
|
1953
|
+
end
|
|
1954
|
+
end
|
|
1955
|
+
|
|
1956
|
+
it 'returns nil' do
|
|
1957
|
+
expect(context.pluck('label..name')).to eq([ nil ])
|
|
1958
|
+
end
|
|
1959
|
+
end
|
|
1829
1960
|
end
|
|
1830
1961
|
|
|
1831
1962
|
describe "#pick" do
|
|
@@ -1886,6 +2017,27 @@ describe Mongoid::Contextual::Memory do
|
|
|
1886
2017
|
expect(picked).to be_nil
|
|
1887
2018
|
end
|
|
1888
2019
|
end
|
|
2020
|
+
|
|
2021
|
+
context 'when the field name is a method name' do
|
|
2022
|
+
let!(:person) do
|
|
2023
|
+
Person.create!(ssn: 'secret-ssn')
|
|
2024
|
+
end
|
|
2025
|
+
|
|
2026
|
+
let!(:address) do
|
|
2027
|
+
person.addresses.create!(street: 'hobrecht')
|
|
2028
|
+
end
|
|
2029
|
+
|
|
2030
|
+
let(:criteria) do
|
|
2031
|
+
Address.all.tap do |crit|
|
|
2032
|
+
crit.documents = [ address ]
|
|
2033
|
+
end
|
|
2034
|
+
end
|
|
2035
|
+
|
|
2036
|
+
it 'does not call the method' do
|
|
2037
|
+
expect(address).not_to receive(:destroy)
|
|
2038
|
+
expect(context.pick(:destroy)).to be_nil
|
|
2039
|
+
end
|
|
2040
|
+
end
|
|
1889
2041
|
end
|
|
1890
2042
|
|
|
1891
2043
|
describe "#tally" do
|
|
@@ -2340,6 +2492,31 @@ describe Mongoid::Contextual::Memory do
|
|
|
2340
2492
|
)
|
|
2341
2493
|
end
|
|
2342
2494
|
end
|
|
2495
|
+
|
|
2496
|
+
context 'when the field name is a method name' do
|
|
2497
|
+
let!(:person) do
|
|
2498
|
+
Person.create!(ssn: 'secret-ssn')
|
|
2499
|
+
end
|
|
2500
|
+
|
|
2501
|
+
let!(:address) do
|
|
2502
|
+
person.addresses.create!(street: 'hobrecht')
|
|
2503
|
+
end
|
|
2504
|
+
|
|
2505
|
+
let(:criteria) do
|
|
2506
|
+
Address.all.tap do |crit|
|
|
2507
|
+
crit.documents = [ address ]
|
|
2508
|
+
end
|
|
2509
|
+
end
|
|
2510
|
+
|
|
2511
|
+
it 'does not call the method' do
|
|
2512
|
+
expect(address).not_to receive(:destroy)
|
|
2513
|
+
expect(context.tally(:destroy)).to eq(nil => 1)
|
|
2514
|
+
end
|
|
2515
|
+
|
|
2516
|
+
it 'does not disclose the parent document fields' do
|
|
2517
|
+
expect(context.tally('_root.ssn')).to eq(nil => 1)
|
|
2518
|
+
end
|
|
2519
|
+
end
|
|
2343
2520
|
end
|
|
2344
2521
|
|
|
2345
2522
|
describe '#inc' do
|
|
@@ -1830,6 +1830,8 @@ describe Mongoid::Criteria::Queryable::Selectable do
|
|
|
1830
1830
|
end
|
|
1831
1831
|
|
|
1832
1832
|
context 'when the following criteria uses string were form' do
|
|
1833
|
+
# String criteria compile to $where, which requires the opt-in.
|
|
1834
|
+
config_override :allow_unsafe_query_operators, true
|
|
1833
1835
|
|
|
1834
1836
|
let(:selection) do
|
|
1835
1837
|
query.not.where('hello world')
|