mongoid 9.1.1 → 9.1.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.
- checksums.yaml +4 -4
- data/lib/config/locales/en.yml +31 -11
- data/lib/mongoid/association/accessors.rb +18 -4
- data/lib/mongoid/association/eager_load/discriminated_inclusion.rb +10 -10
- data/lib/mongoid/association/eager_load/embedded_distributor.rb +6 -6
- data/lib/mongoid/association/eager_load/lookup_pipeline.rb +1 -1
- data/lib/mongoid/association/eager_loadable.rb +8 -1
- data/lib/mongoid/association/embedded/batchable.rb +17 -1
- data/lib/mongoid/association/nested/many.rb +1 -1
- data/lib/mongoid/association/referenced/has_and_belongs_to_many/buildable.rb +6 -0
- data/lib/mongoid/association/relatable.rb +18 -0
- data/lib/mongoid/association.rb +8 -0
- data/lib/mongoid/atomic/modifiers.rb +100 -16
- data/lib/mongoid/clients/factory.rb +2 -0
- data/lib/mongoid/config/encryption.rb +148 -17
- data/lib/mongoid/document.rb +3 -0
- data/lib/mongoid/encryptable.rb +42 -15
- data/lib/mongoid/errors/invalid_encryption_schema.rb +27 -0
- data/lib/mongoid/errors/no_encryption_schema.rb +5 -3
- data/lib/mongoid/errors.rb +1 -0
- data/lib/mongoid/fields.rb +7 -0
- data/lib/mongoid/persistable/creatable.rb +120 -14
- data/lib/mongoid/persistable.rb +15 -15
- data/lib/mongoid/persistence_context.rb +46 -4
- data/lib/mongoid/search_indexable.rb +5 -1
- data/lib/mongoid/tasks/database.rb +10 -7
- data/lib/mongoid/touchable.rb +28 -1
- data/lib/mongoid/version.rb +1 -1
- data/lib/mongoid/warnings.rb +4 -0
- data/spec/integration/associations/embedded_delayed_atomic_sets_spec.rb +216 -0
- data/spec/integration/associations/has_and_belongs_to_many_spec.rb +14 -0
- data/spec/integration/atomic/modifiers_spec.rb +283 -0
- data/spec/integration/encryption_spec.rb +149 -0
- data/spec/mongoid/association/accessors_spec.rb +35 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many/buildable_spec.rb +11 -0
- data/spec/mongoid/atomic/modifiers_spec.rb +347 -0
- data/spec/mongoid/attributes/nested_spec.rb +29 -0
- data/spec/mongoid/clients/factory_spec.rb +36 -0
- data/spec/mongoid/config/encryption_spec.rb +223 -0
- data/spec/mongoid/criteria/includable_spec.rb +346 -0
- data/spec/mongoid/encryptable_spec.rb +54 -0
- data/spec/mongoid/fields_spec.rb +8 -0
- data/spec/mongoid/search_indexable_spec.rb +26 -0
- data/spec/mongoid/tasks/database_spec.rb +13 -0
- data/spec/mongoid/touchable_spec.rb +217 -0
- data/spec/mongoid/touchable_spec_models.rb +37 -0
- data/spec/support/crypt/models.rb +96 -0
- data/spec/support/models/modifier_models.rb +65 -0
- metadata +5 -2
|
@@ -245,4 +245,153 @@ describe 'Encryption' do
|
|
|
245
245
|
end
|
|
246
246
|
end
|
|
247
247
|
end
|
|
248
|
+
|
|
249
|
+
# The schema map has one entry per namespace, and it has to cover every model
|
|
250
|
+
# that writes there: models sharing a collection, subclasses, and subclasses
|
|
251
|
+
# of embedded models. Documents are read back through a client without
|
|
252
|
+
# automatic encryption, to see what is actually stored.
|
|
253
|
+
describe 'when more than one model writes to a namespace' do
|
|
254
|
+
let(:root_models) do
|
|
255
|
+
[ Crypt::Badge, Crypt::BadgeScan, Crypt::Member, Crypt::Listing, Crypt::Box, Crypt::Locker ]
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
let(:keyed_models) do
|
|
259
|
+
[ Crypt::Badge, Crypt::BadgeScan, Crypt::Member, Crypt::PrivateListing, Crypt::Box, Crypt::SecretContent ]
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
around do |example|
|
|
263
|
+
original_options = root_models.to_h { |model| [ model, model.storage_options ] }
|
|
264
|
+
existing_key_ids = keyed_models.to_h { |model| [ model, model.encrypt_metadata[:key_id] ] }
|
|
265
|
+
keyed_models.each { |model| model.set_key_id(data_key_id) }
|
|
266
|
+
root_models.each do |model|
|
|
267
|
+
unencrypted_client[model.collection_name.to_s].drop
|
|
268
|
+
model.store_in(client: :encrypted)
|
|
269
|
+
end
|
|
270
|
+
# The schema map is generated when the client is built, so the client has
|
|
271
|
+
# to be built after the key ids above are in place.
|
|
272
|
+
Mongoid::Clients.clear
|
|
273
|
+
|
|
274
|
+
example.run
|
|
275
|
+
|
|
276
|
+
original_options.each { |model, options| model.storage_options = options }
|
|
277
|
+
existing_key_ids.each { |model, key_id| model.set_key_id(key_id) }
|
|
278
|
+
Mongoid::Clients.clear
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def stored_document(model, id)
|
|
282
|
+
unencrypted_client[model.collection_name.to_s].find(_id: id).first
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def expect_ciphertext(value)
|
|
286
|
+
expect(value).to be_a(BSON::Binary).and(have_attributes(type: :ciphertext))
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
context 'when two models are stored in one collection' do
|
|
290
|
+
it 'stores the fields of both models encrypted', :aggregate_failures do
|
|
291
|
+
badge = Crypt::Badge.create!(serial: 'SERIAL-1')
|
|
292
|
+
scan = Crypt::BadgeScan.create!(holder: 'HOLDER-1')
|
|
293
|
+
|
|
294
|
+
expect_ciphertext(stored_document(Crypt::Badge, badge.id)['serial'])
|
|
295
|
+
expect_ciphertext(stored_document(Crypt::BadgeScan, scan.id)['holder'])
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
context 'when a subclass adds an encrypted field to an encrypted parent' do
|
|
300
|
+
it 'stores the fields of the parent and of the subclass encrypted', :aggregate_failures do
|
|
301
|
+
employee = Crypt::Employee.create!(ssn: 'SSN-1', salary: 'SALARY-1')
|
|
302
|
+
|
|
303
|
+
stored_document(Crypt::Member, employee.id).tap do |doc|
|
|
304
|
+
expect_ciphertext(doc['ssn'])
|
|
305
|
+
expect_ciphertext(doc['salary'])
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
it 'stores the fields of a sibling subclass encrypted' do
|
|
310
|
+
contractor = Crypt::Contractor.create!(ssn: 'SSN-2', rate: 'RATE-1')
|
|
311
|
+
|
|
312
|
+
expect_ciphertext(stored_document(Crypt::Member, contractor.id)['rate'])
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
it 'reads the fields back' do
|
|
316
|
+
employee = Crypt::Employee.create!(ssn: 'SSN-3', salary: 'SALARY-3')
|
|
317
|
+
|
|
318
|
+
expect(Crypt::Member.find(employee.id).salary).to eq('SALARY-3')
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
context 'when only a subclass has encrypted fields' do
|
|
323
|
+
it 'stores the fields of the subclass encrypted' do
|
|
324
|
+
listing = Crypt::PrivateListing.create!(title: 'TITLE-1', owner_ssn: 'SSN-4')
|
|
325
|
+
|
|
326
|
+
expect_ciphertext(stored_document(Crypt::Listing, listing.id)['owner_ssn'])
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
context 'when a subclass of an embeds_one target adds an encrypted field' do
|
|
331
|
+
it 'stores the fields of the target and of the subclass encrypted', :aggregate_failures do
|
|
332
|
+
box = Crypt::Box.create!(item: Crypt::SealedItem.new(label: 'LABEL-1', seal: 'SEAL-1'))
|
|
333
|
+
|
|
334
|
+
stored_document(Crypt::Box, box.id)['item'].tap do |item|
|
|
335
|
+
expect_ciphertext(item['label'])
|
|
336
|
+
expect_ciphertext(item['seal'])
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
context 'when only a subclass of an embeds_one target has encrypted fields' do
|
|
342
|
+
it 'stores the fields of the subclass encrypted' do
|
|
343
|
+
locker = Crypt::Locker.create!(content: Crypt::SecretContent.new(label: 'LABEL-2', code: 'CODE-1'))
|
|
344
|
+
|
|
345
|
+
expect_ciphertext(stored_document(Crypt::Locker, locker.id)['content']['code'])
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# An entry for the namespace exists, but it does not cover every encrypted
|
|
350
|
+
# field of the model. The driver encrypts what the entry lists and writes
|
|
351
|
+
# the rest in plaintext.
|
|
352
|
+
context 'when the schema entry for the namespace misses an encrypted field' do
|
|
353
|
+
let(:config) do
|
|
354
|
+
super().merge(
|
|
355
|
+
partial: {
|
|
356
|
+
hosts: SpecConfig.instance.addresses,
|
|
357
|
+
database: database_id,
|
|
358
|
+
options: {
|
|
359
|
+
auto_encryption_options: {
|
|
360
|
+
key_vault_client: :key_vault,
|
|
361
|
+
kms_providers: kms_providers,
|
|
362
|
+
key_vault_namespace: key_vault_namespace,
|
|
363
|
+
extra_options: extra_options,
|
|
364
|
+
schema_map: partial_schema_map
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
let(:partial_schema_map) do
|
|
372
|
+
{
|
|
373
|
+
"#{database_id}.#{Crypt::Member.collection_name}" => {
|
|
374
|
+
'bsonType' => 'object',
|
|
375
|
+
'encryptMetadata' => {
|
|
376
|
+
'keyId' => [ BSON::Binary.new(Base64.decode64(data_key_id), :uuid) ],
|
|
377
|
+
'algorithm' => 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
|
|
378
|
+
},
|
|
379
|
+
'properties' => {
|
|
380
|
+
'ssn' => { 'encrypt' => { 'bsonType' => 'string' } }
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
it 'does not store the field in plaintext', :aggregate_failures do
|
|
387
|
+
expect do
|
|
388
|
+
Crypt::Employee.with(client: :partial) { |klass| klass.create!(ssn: 'SSN-5', salary: 'PARTIAL-1') }
|
|
389
|
+
end.to raise_error(Mongoid::Errors::NoEncryptionSchema)
|
|
390
|
+
|
|
391
|
+
expect(
|
|
392
|
+
unencrypted_client[Crypt::Member.collection_name.to_s].find(salary: 'PARTIAL-1').to_a
|
|
393
|
+
).to be_empty
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
end
|
|
248
397
|
end
|
|
@@ -689,6 +689,41 @@ describe Mongoid::Association::Accessors do
|
|
|
689
689
|
end
|
|
690
690
|
end
|
|
691
691
|
end
|
|
692
|
+
|
|
693
|
+
context 'when the document has a stored field named like a referenced association' do
|
|
694
|
+
before(:all) do
|
|
695
|
+
class Musician
|
|
696
|
+
include Mongoid::Document
|
|
697
|
+
|
|
698
|
+
belongs_to :orchestra, optional: true
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
class Orchestra
|
|
702
|
+
include Mongoid::Document
|
|
703
|
+
|
|
704
|
+
has_many :musicians
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
after(:all) do
|
|
709
|
+
Object.send(:remove_const, :Orchestra)
|
|
710
|
+
Object.send(:remove_const, :Musician)
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
# The musicians used to be embedded, so the stored orchestra still holds
|
|
714
|
+
# the old array under the association's name.
|
|
715
|
+
let!(:orchestra_id) do
|
|
716
|
+
BSON::ObjectId.new.tap do |id|
|
|
717
|
+
Orchestra.collection.insert_one('_id' => id, 'musicians' => [ { '_id' => BSON::ObjectId.new } ])
|
|
718
|
+
end
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
let!(:musician) { Musician.create!(orchestra_id: orchestra_id) }
|
|
722
|
+
|
|
723
|
+
it 'loads the association by its key' do
|
|
724
|
+
expect(Orchestra.find(orchestra_id).musicians.to_a).to eq([ musician ])
|
|
725
|
+
end
|
|
726
|
+
end
|
|
692
727
|
end
|
|
693
728
|
|
|
694
729
|
context 'when setting associations to empty values' do
|
|
@@ -107,6 +107,17 @@ describe Mongoid::Association::Referenced::HasAndBelongsToMany::Buildable do
|
|
|
107
107
|
end
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
+
context 'when provided an empty array' do
|
|
111
|
+
let(:object) do
|
|
112
|
+
[]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'returns an empty criteria' do
|
|
116
|
+
expect(documents).to be_a(Mongoid::Criteria)
|
|
117
|
+
expect(documents.empty_and_chainable?).to be true
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
110
121
|
context 'when provided a object' do
|
|
111
122
|
context 'when the object is not nil' do
|
|
112
123
|
let(:object) do
|
|
@@ -74,6 +74,78 @@ describe Mongoid::Atomic::Modifiers do
|
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
|
+
|
|
78
|
+
context 'when a conflicting modification exists' do
|
|
79
|
+
context 'when the conflicting modification is a set' do
|
|
80
|
+
let(:sets) do
|
|
81
|
+
{ 'addresses.0.street' => 'Bond' }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
let(:adds) do
|
|
85
|
+
{ 'addresses.0.tags' => %w[tag] }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
before do
|
|
89
|
+
modifiers.set(sets)
|
|
90
|
+
modifiers.add_to_set(adds)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'adds the add to set modifiers to the conflicts hash' do
|
|
94
|
+
expect(modifiers).to eq(
|
|
95
|
+
{ '$set' => { 'addresses.0.street' => 'Bond' },
|
|
96
|
+
conflicts: { '$addToSet' =>
|
|
97
|
+
{ 'addresses.0.tags' => { '$each' => %w[tag] } } } }
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
context 'when the conflicting modification is a push' do
|
|
103
|
+
let(:pushes) do
|
|
104
|
+
{ 'addresses' => { 'street' => 'Oxford St' } }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
let(:adds) do
|
|
108
|
+
{ 'addresses.0.tags' => %w[tag] }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
before do
|
|
112
|
+
modifiers.push(pushes)
|
|
113
|
+
modifiers.add_to_set(adds)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'adds the add to set modifiers to the conflicts hash' do
|
|
117
|
+
expect(modifiers).to eq(
|
|
118
|
+
{ '$push' => { 'addresses' => { '$each' => [ { 'street' => 'Oxford St' } ] } },
|
|
119
|
+
conflicts: { '$addToSet' =>
|
|
120
|
+
{ 'addresses.0.tags' => { '$each' => %w[tag] } } } }
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
context 'when the conflicting modification is a pull' do
|
|
126
|
+
let(:pulls) do
|
|
127
|
+
{ 'addresses' => { '_id' => { '$in' => [ 'one' ] } } }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
let(:adds) do
|
|
131
|
+
{ 'addresses.0.tags' => %w[tag] }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
before do
|
|
135
|
+
modifiers.pull(pulls)
|
|
136
|
+
modifiers.add_to_set(adds)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it 'does not split the operations, leaving MongoDB to reject the update' do
|
|
140
|
+
# A $pull shifts array elements, so applying the $addToSet in a
|
|
141
|
+
# separate write afterwards could target a different element.
|
|
142
|
+
expect(modifiers).to eq(
|
|
143
|
+
{ '$pull' => { 'addresses' => { '_id' => { '$in' => [ 'one' ] } } },
|
|
144
|
+
'$addToSet' => { 'addresses.0.tags' => { '$each' => %w[tag] } } }
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
77
149
|
end
|
|
78
150
|
|
|
79
151
|
describe '#pull' do
|
|
@@ -125,6 +197,98 @@ describe Mongoid::Atomic::Modifiers do
|
|
|
125
197
|
end
|
|
126
198
|
end
|
|
127
199
|
end
|
|
200
|
+
|
|
201
|
+
context 'when a conflicting modification exists' do
|
|
202
|
+
# A $pull shifts the positions of the remaining elements, so it stays in
|
|
203
|
+
# the main update. MongoDB rejects the update if the paths overlap.
|
|
204
|
+
context 'when the conflicting modification is a set' do
|
|
205
|
+
let(:sets) do
|
|
206
|
+
{ 'addresses.0.street' => 'Bond' }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
let(:pulls) do
|
|
210
|
+
{ 'addresses' => { '_id' => { '$in' => [ 'one' ] } } }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
before do
|
|
214
|
+
modifiers.set(sets)
|
|
215
|
+
modifiers.pull(pulls)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
it 'keeps the pull modifiers in the main update' do
|
|
219
|
+
expect(modifiers).to eq(
|
|
220
|
+
{ '$set' => { 'addresses.0.street' => 'Bond' },
|
|
221
|
+
'$pull' => { 'addresses' => { '_id' => { '$in' => [ 'one' ] } } } }
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
context 'when the conflicting modification is a push' do
|
|
227
|
+
let(:pushes) do
|
|
228
|
+
{ 'addresses' => { 'street' => 'Oxford St' } }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
let(:pulls) do
|
|
232
|
+
{ 'addresses' => { '_id' => { '$in' => [ 'one' ] } } }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
before do
|
|
236
|
+
modifiers.push(pushes)
|
|
237
|
+
modifiers.pull(pulls)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
it 'keeps the pull modifiers in the main update' do
|
|
241
|
+
expect(modifiers).to eq(
|
|
242
|
+
{ '$push' => { 'addresses' => { '$each' => [ { 'street' => 'Oxford St' } ] } },
|
|
243
|
+
'$pull' => { 'addresses' => { '_id' => { '$in' => [ 'one' ] } } } }
|
|
244
|
+
)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
context 'when the conflicting modification is an add to set' do
|
|
249
|
+
let(:adds) do
|
|
250
|
+
{ 'addresses.0.tags' => %w[tag] }
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
let(:pulls) do
|
|
254
|
+
{ 'addresses' => { '_id' => { '$in' => [ 'one' ] } } }
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
before do
|
|
258
|
+
modifiers.add_to_set(adds)
|
|
259
|
+
modifiers.pull(pulls)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it 'keeps the pull modifiers in the main update' do
|
|
263
|
+
expect(modifiers).to eq(
|
|
264
|
+
{ '$addToSet' => { 'addresses.0.tags' => { '$each' => %w[tag] } },
|
|
265
|
+
'$pull' => { 'addresses' => { '_id' => { '$in' => [ 'one' ] } } } }
|
|
266
|
+
)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
context 'when the conflicting modification is a pull all' do
|
|
271
|
+
let(:pull_alls) do
|
|
272
|
+
{ 'addresses' => [ { '_id' => 'one' } ] }
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
let(:pulls) do
|
|
276
|
+
{ 'addresses' => { '_id' => { '$in' => [ 'one' ] } } }
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
before do
|
|
280
|
+
modifiers.pull_all(pull_alls)
|
|
281
|
+
modifiers.pull(pulls)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
it 'keeps the pull modifiers in the main update' do
|
|
285
|
+
expect(modifiers).to eq(
|
|
286
|
+
{ '$pullAll' => { 'addresses' => [ { '_id' => 'one' } ] },
|
|
287
|
+
'$pull' => { 'addresses' => { '_id' => { '$in' => [ 'one' ] } } } }
|
|
288
|
+
)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
128
292
|
end
|
|
129
293
|
|
|
130
294
|
describe '#pull_all' do
|
|
@@ -183,6 +347,54 @@ describe Mongoid::Atomic::Modifiers do
|
|
|
183
347
|
end
|
|
184
348
|
end
|
|
185
349
|
end
|
|
350
|
+
|
|
351
|
+
context 'when a conflicting modification exists' do
|
|
352
|
+
context 'when the conflicting modification is a pull' do
|
|
353
|
+
let(:pulls) do
|
|
354
|
+
{ 'addresses' => { '_id' => { '$in' => [ 'one' ] } } }
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
let(:pull_alls) do
|
|
358
|
+
{ 'addresses' => [ { '_id' => 'one' } ] }
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
before do
|
|
362
|
+
modifiers.pull(pulls)
|
|
363
|
+
modifiers.pull_all(pull_alls)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
it 'adds the pull all modifiers to the conflicts hash' do
|
|
367
|
+
expect(modifiers).to eq(
|
|
368
|
+
{ '$pull' => { 'addresses' => { '_id' => { '$in' => [ 'one' ] } } },
|
|
369
|
+
conflicts: { '$pullAll' =>
|
|
370
|
+
{ 'addresses' => [ { '_id' => 'one' } ] } } }
|
|
371
|
+
)
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
context 'when the conflicting modification is a push' do
|
|
376
|
+
let(:pushes) do
|
|
377
|
+
{ 'addresses' => { 'street' => 'Oxford St' } }
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
let(:pull_alls) do
|
|
381
|
+
{ 'addresses' => [ { '_id' => 'one' } ] }
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
before do
|
|
385
|
+
modifiers.push(pushes)
|
|
386
|
+
modifiers.pull_all(pull_alls)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
it 'adds the pull all modifiers to the conflicts hash' do
|
|
390
|
+
expect(modifiers).to eq(
|
|
391
|
+
{ '$push' => { 'addresses' => { '$each' => [ { 'street' => 'Oxford St' } ] } },
|
|
392
|
+
conflicts: { '$pullAll' =>
|
|
393
|
+
{ 'addresses' => [ { '_id' => 'one' } ] } } }
|
|
394
|
+
)
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
end
|
|
186
398
|
end
|
|
187
399
|
|
|
188
400
|
describe '#push' do
|
|
@@ -321,6 +533,56 @@ describe Mongoid::Atomic::Modifiers do
|
|
|
321
533
|
)
|
|
322
534
|
end
|
|
323
535
|
end
|
|
536
|
+
|
|
537
|
+
context 'when the conflicting modification is an add to set' do
|
|
538
|
+
let(:adds) do
|
|
539
|
+
{ 'addresses.0.tags' => %w[tag] }
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
let(:pushes) do
|
|
543
|
+
{ 'addresses' => { 'street' => 'Oxford St' } }
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
before do
|
|
547
|
+
modifiers.add_to_set(adds)
|
|
548
|
+
modifiers.push(pushes)
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
it 'adds the push modifiers to the conflicts hash' do
|
|
552
|
+
expect(modifiers).to eq(
|
|
553
|
+
{ '$addToSet' => { 'addresses.0.tags' => { '$each' => %w[tag] } },
|
|
554
|
+
conflicts: { '$push' =>
|
|
555
|
+
{ 'addresses' => { '$each' => [
|
|
556
|
+
{ 'street' => 'Oxford St' }
|
|
557
|
+
] } } } }
|
|
558
|
+
)
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
context 'when the conflicting modification is an unset' do
|
|
563
|
+
let(:unsets) do
|
|
564
|
+
[ 'addresses.0.street' ]
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
let(:pushes) do
|
|
568
|
+
{ 'addresses' => { 'street' => 'Oxford St' } }
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
before do
|
|
572
|
+
modifiers.unset(unsets)
|
|
573
|
+
modifiers.push(pushes)
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
it 'adds the push modifiers to the conflicts hash' do
|
|
577
|
+
expect(modifiers).to eq(
|
|
578
|
+
{ '$unset' => { 'addresses.0.street' => true },
|
|
579
|
+
conflicts: { '$push' =>
|
|
580
|
+
{ 'addresses' => { '$each' => [
|
|
581
|
+
{ 'street' => 'Oxford St' }
|
|
582
|
+
] } } } }
|
|
583
|
+
)
|
|
584
|
+
end
|
|
585
|
+
end
|
|
324
586
|
end
|
|
325
587
|
|
|
326
588
|
context 'when there is another operation on a sibling of a key which is nested' do
|
|
@@ -419,6 +681,30 @@ describe Mongoid::Atomic::Modifiers do
|
|
|
419
681
|
)
|
|
420
682
|
end
|
|
421
683
|
end
|
|
684
|
+
|
|
685
|
+
context 'when the conflicting modification is an add to set' do
|
|
686
|
+
let(:adds) do
|
|
687
|
+
{ 'addresses.0.tags' => %w[tag] }
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
let(:sets) do
|
|
691
|
+
{ 'addresses.0.title' => 'Sir' }
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
before do
|
|
695
|
+
modifiers.add_to_set(adds)
|
|
696
|
+
modifiers.set(sets)
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
it 'adds the set modifiers to the conflicts hash' do
|
|
700
|
+
expect(modifiers).to eq(
|
|
701
|
+
{ '$addToSet' =>
|
|
702
|
+
{ 'addresses.0.tags' => { '$each' => %w[tag] } },
|
|
703
|
+
conflicts:
|
|
704
|
+
{ '$set' => { 'addresses.0.title' => 'Sir' } } }
|
|
705
|
+
)
|
|
706
|
+
end
|
|
707
|
+
end
|
|
422
708
|
end
|
|
423
709
|
end
|
|
424
710
|
|
|
@@ -488,6 +774,67 @@ describe Mongoid::Atomic::Modifiers do
|
|
|
488
774
|
end
|
|
489
775
|
end
|
|
490
776
|
end
|
|
777
|
+
|
|
778
|
+
context 'when the unsets conflict with a prior push' do
|
|
779
|
+
# A $push appends to the array and does not shift existing element
|
|
780
|
+
# positions, so the $unset can be applied safely in a separate write.
|
|
781
|
+
let(:pushes) { { 'books' => { 'title' => '1984' } } }
|
|
782
|
+
|
|
783
|
+
let(:unsets) { [ 'books.0.title' ] }
|
|
784
|
+
|
|
785
|
+
before do
|
|
786
|
+
modifiers.push(pushes)
|
|
787
|
+
modifiers.unset(unsets)
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
it 'adds the unset modifiers to the conflicts hash' do
|
|
791
|
+
expect(modifiers).to eq(
|
|
792
|
+
{ '$push' => { 'books' => { '$each' => [ { 'title' => '1984' } ] } },
|
|
793
|
+
conflicts: { '$unset' => { 'books.0.title' => true } } }
|
|
794
|
+
)
|
|
795
|
+
end
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
context 'when the unsets conflict with a prior add to set' do
|
|
799
|
+
let(:adds) { { 'books.0.tags' => %w[tag] } }
|
|
800
|
+
|
|
801
|
+
let(:unsets) { [ 'books.0.title' ] }
|
|
802
|
+
|
|
803
|
+
before do
|
|
804
|
+
modifiers.add_to_set(adds)
|
|
805
|
+
modifiers.unset(unsets)
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
it 'adds the unset modifiers to the conflicts hash' do
|
|
809
|
+
expect(modifiers).to eq(
|
|
810
|
+
{ '$addToSet' => { 'books.0.tags' => { '$each' => %w[tag] } },
|
|
811
|
+
conflicts: { '$unset' => { 'books.0.title' => true } } }
|
|
812
|
+
)
|
|
813
|
+
end
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
context 'when there is a pending pull on the same field' do
|
|
817
|
+
# A $pull shifts array elements, so routing the $unset into a separate
|
|
818
|
+
# write could make it target a different element. The operations are
|
|
819
|
+
# left together so MongoDB rejects the update (code 40) atomically.
|
|
820
|
+
let(:pulls) do
|
|
821
|
+
{ 'books' => { '_id' => { '$in' => [ 'one' ] } } }
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
let(:unsets) { [ 'books.0.title' ] }
|
|
825
|
+
|
|
826
|
+
before do
|
|
827
|
+
modifiers.pull(pulls)
|
|
828
|
+
modifiers.unset(unsets)
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
it 'does not split the operations' do
|
|
832
|
+
expect(modifiers).to eq(
|
|
833
|
+
{ '$pull' => { 'books' => { '_id' => { '$in' => [ 'one' ] } } },
|
|
834
|
+
'$unset' => { 'books.0.title' => true } }
|
|
835
|
+
)
|
|
836
|
+
end
|
|
837
|
+
end
|
|
491
838
|
end
|
|
492
839
|
end
|
|
493
840
|
end
|
|
@@ -290,6 +290,35 @@ describe Mongoid::Attributes::Nested do
|
|
|
290
290
|
end
|
|
291
291
|
end
|
|
292
292
|
|
|
293
|
+
context 'when a has-many association uses a custom primary key' do
|
|
294
|
+
# A throwaway class, so that accepts_nested_attributes_for does not
|
|
295
|
+
# leave autosave callbacks and nested attributes on a shared model.
|
|
296
|
+
let(:company_class) do
|
|
297
|
+
stub_const('NestedSpecCompany', Class.new do
|
|
298
|
+
include Mongoid::Document
|
|
299
|
+
|
|
300
|
+
store_in collection: 'hmm_companies'
|
|
301
|
+
field :p, type: Integer
|
|
302
|
+
has_many :emails, primary_key: :p, foreign_key: :f, class_name: 'HmmEmail', inverse_of: nil
|
|
303
|
+
accepts_nested_attributes_for :emails
|
|
304
|
+
end)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
after do
|
|
308
|
+
Mongoid.deregister_model(company_class)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
it 'reports the parent primary key value in the error params' do
|
|
312
|
+
company = company_class.create!(p: 42)
|
|
313
|
+
|
|
314
|
+
expect do
|
|
315
|
+
company.emails_attributes = { '0' => { 'id' => 'missing', 'f' => 42 } }
|
|
316
|
+
end.to raise_error(Mongoid::Errors::DocumentNotFound) { |error|
|
|
317
|
+
expect(error.params).to eq({ _id: 'missing', 'f' => 42 })
|
|
318
|
+
}
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
293
322
|
context 'when the relation is a has-and-belongs-to-many' do
|
|
294
323
|
before do
|
|
295
324
|
Person.send(:undef_method, :preferences_attributes=)
|
|
@@ -331,6 +331,42 @@ describe Mongoid::Clients::Factory do
|
|
|
331
331
|
end
|
|
332
332
|
end
|
|
333
333
|
end
|
|
334
|
+
|
|
335
|
+
context 'when bypass_auto_encryption is set' do
|
|
336
|
+
it 'warns that encryption is bypassed' do
|
|
337
|
+
expect(Mongoid::Warnings).to receive(:warn_bypass_auto_encryption)
|
|
338
|
+
|
|
339
|
+
described_class.send(
|
|
340
|
+
:build_auto_encryption_options,
|
|
341
|
+
{ auto_encryption_options: { bypass_auto_encryption: true, schema_map: {} } },
|
|
342
|
+
database_id
|
|
343
|
+
)
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
context 'when bypass_auto_encryption is not set' do
|
|
348
|
+
it 'does not warn' do
|
|
349
|
+
expect(Mongoid::Warnings).not_to receive(:warn_bypass_auto_encryption)
|
|
350
|
+
|
|
351
|
+
described_class.send(
|
|
352
|
+
:build_auto_encryption_options,
|
|
353
|
+
{ auto_encryption_options: { schema_map: {} } },
|
|
354
|
+
database_id
|
|
355
|
+
)
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
context 'when bypass_auto_encryption is false' do
|
|
360
|
+
it 'does not warn' do
|
|
361
|
+
expect(Mongoid::Warnings).not_to receive(:warn_bypass_auto_encryption)
|
|
362
|
+
|
|
363
|
+
described_class.send(
|
|
364
|
+
:build_auto_encryption_options,
|
|
365
|
+
{ auto_encryption_options: { bypass_auto_encryption: false, schema_map: {} } },
|
|
366
|
+
database_id
|
|
367
|
+
)
|
|
368
|
+
end
|
|
369
|
+
end
|
|
334
370
|
end
|
|
335
371
|
|
|
336
372
|
context 'when no name is provided' do
|