couchbase-orm 3.1.0 → 3.3.0

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.
data/spec/base_spec.rb CHANGED
@@ -22,6 +22,33 @@ class BaseTestWithIgnoredProperties < CouchbaseOrm::Base
22
22
  attribute :job, :string
23
23
  end
24
24
 
25
+ class BaseTestWithUnknownAttributesAllowed < CouchbaseOrm::Base
26
+ self.raise_on_unknown_attributes = false
27
+ attribute :name, :string
28
+ attribute :job, :string
29
+ end
30
+
31
+ class NestedDocWithUnknownAttributesAllowed < CouchbaseOrm::NestedDocument
32
+ self.raise_on_unknown_attributes = false
33
+ attribute :title, :string
34
+ attribute :value, :integer
35
+ end
36
+
37
+ class NestedDocWithDefaultBehavior < CouchbaseOrm::NestedDocument
38
+ attribute :title, :string
39
+ attribute :value, :integer
40
+ end
41
+
42
+ class ParentDocWithNestedUnknownAllowed < CouchbaseOrm::Base
43
+ attribute :name, :string
44
+ attribute :nested_item, :nested, type: NestedDocWithUnknownAttributesAllowed
45
+ end
46
+
47
+ class ParentDocWithNestedDefault < CouchbaseOrm::Base
48
+ attribute :name, :string
49
+ attribute :nested_item, :nested, type: NestedDocWithDefaultBehavior
50
+ end
51
+
25
52
  class BaseTestWithPropertiesAlwaysExistsInDocument < CouchbaseOrm::Base
26
53
  self.properties_always_exists_in_document = true
27
54
  attribute :name, :string
@@ -349,6 +376,116 @@ describe CouchbaseOrm::Base do
349
376
  end
350
377
  end
351
378
 
379
+ describe 'handling unknown attributes' do
380
+ context 'when raise_on_unknown_attributes is set to false' do
381
+ it 'returns false when queried' do
382
+ expect(BaseTestWithUnknownAttributesAllowed.raise_on_unknown_attributes).to be(false)
383
+ end
384
+
385
+ it 'silently ignores unknown attributes in new' do
386
+ model = BaseTestWithUnknownAttributesAllowed.new(name: 'test', job: 'dev', unknown_attr: 'value')
387
+ expect(model.name).to eq('test')
388
+ expect(model.job).to eq('dev')
389
+ expect(model.respond_to?(:unknown_attr)).to be(false)
390
+ end
391
+
392
+ it 'silently ignores unknown attributes in assign_attributes' do
393
+ model = BaseTestWithUnknownAttributesAllowed.new(name: 'test')
394
+ expect {
395
+ model.assign_attributes(name: 'updated', job: 'engineer', foo: 'bar', baz: 'qux')
396
+ }.not_to raise_error
397
+ expect(model.name).to eq('updated')
398
+ expect(model.job).to eq('engineer')
399
+ expect(model.respond_to?(:foo)).to be(false)
400
+ expect(model.respond_to?(:baz)).to be(false)
401
+ end
402
+
403
+ it 'only stores known attributes' do
404
+ model = BaseTestWithUnknownAttributesAllowed.new(
405
+ name: 'Alice',
406
+ job: 'Developer',
407
+ unknown_field_1: 'value1',
408
+ unknown_field_2: 'value2'
409
+ )
410
+ # Only known attributes should be stored
411
+ expect(model.name).to eq('Alice')
412
+ expect(model.job).to eq('Developer')
413
+ expect(model.respond_to?(:unknown_field_1)).to be(false)
414
+ expect(model.respond_to?(:unknown_field_2)).to be(false)
415
+ end
416
+ end
417
+
418
+ context 'default behavior (raise_on_unknown_attributes = true)' do
419
+ it 'returns true by default' do
420
+ expect(BaseTest.raise_on_unknown_attributes).to be(true)
421
+ end
422
+
423
+ it 'raises ActiveModel::UnknownAttributeError on unknown attributes in new' do
424
+ expect {
425
+ BaseTest.new(name: 'bob', job: 'dev', foo: 'bar')
426
+ }.to raise_error(ActiveModel::UnknownAttributeError)
427
+ end
428
+
429
+ it 'raises ActiveModel::UnknownAttributeError on unknown attributes in assign_attributes' do
430
+ model = BaseTest.new(name: 'bob')
431
+ expect {
432
+ model.assign_attributes(job: 'dev', foo: 'bar')
433
+ }.to raise_error(ActiveModel::UnknownAttributeError)
434
+ end
435
+ end
436
+
437
+ context 'for NestedDocument classes' do
438
+ it 'returns false when queried on NestedDocument with raise_on_unknown_attributes = false' do
439
+ expect(NestedDocWithUnknownAttributesAllowed.raise_on_unknown_attributes).to be(false)
440
+ end
441
+
442
+ it 'returns true by default on NestedDocument' do
443
+ expect(NestedDocWithDefaultBehavior.raise_on_unknown_attributes).to be(true)
444
+ end
445
+
446
+ it 'silently ignores unknown attributes in NestedDocument when disabled' do
447
+ nested = NestedDocWithUnknownAttributesAllowed.new(title: 'Test', value: 42, unknown: 'ignored')
448
+ expect(nested.title).to eq('Test')
449
+ expect(nested.value).to eq(42)
450
+ expect(nested.respond_to?(:unknown)).to be(false)
451
+ end
452
+
453
+ it 'raises error for unknown attributes in NestedDocument when enabled' do
454
+ expect {
455
+ NestedDocWithDefaultBehavior.new(title: 'Test', value: 42, unknown: 'error')
456
+ }.to raise_error(ActiveModel::UnknownAttributeError)
457
+ end
458
+
459
+ it 'silently ignores unknown attributes in nested documents within parent' do
460
+ parent = ParentDocWithNestedUnknownAllowed.new(name: 'Parent')
461
+ nested = NestedDocWithUnknownAttributesAllowed.new(title: 'Nested', value: 100, extra: 'ignored')
462
+ parent.nested_item = nested
463
+
464
+ expect(parent.nested_item.title).to eq('Nested')
465
+ expect(parent.nested_item.value).to eq(100)
466
+ expect(parent.nested_item.respond_to?(:extra)).to be(false)
467
+ end
468
+
469
+ it 'raises error for unknown attributes in nested documents with default behavior' do
470
+ parent = ParentDocWithNestedDefault.new(name: 'Parent')
471
+ expect {
472
+ NestedDocWithDefaultBehavior.new(title: 'Nested', value: 100, extra: 'error')
473
+ }.to raise_error(ActiveModel::UnknownAttributeError)
474
+ end
475
+
476
+ it 'works with assign_attributes on NestedDocument' do
477
+ nested = NestedDocWithUnknownAttributesAllowed.new(title: 'Initial')
478
+ expect {
479
+ nested.assign_attributes(title: 'Updated', value: 50, unknown_field: 'ignored')
480
+ }.not_to raise_error
481
+
482
+ expect(nested.title).to eq('Updated')
483
+ expect(nested.value).to eq(50)
484
+ expect(nested.respond_to?(:unknown_field)).to be(false)
485
+ end
486
+ end
487
+ end
488
+
352
489
  describe '.properties_always_exists_in_document' do
353
490
  it 'Uses NOT VALUED when properties_always_exists_in_document = false' do
354
491
  where_clause = BaseTest.where(name: nil)
@@ -3,6 +3,18 @@ require './lib/couchbase-orm/active_record_compat'
3
3
  class Foo
4
4
  include CouchbaseOrm::ActiveRecordCompat
5
5
 
6
+ def self.attribute_names
7
+ %w[id compute_age]
8
+ end
9
+
10
+ def self.attribute_types
11
+ { 'id' => :string }
12
+ end
13
+
14
+ def attribute_names
15
+ self.class.attribute_names
16
+ end
17
+
6
18
  def compute_age
7
19
  10 + 32
8
20
  end
@@ -21,4 +33,65 @@ describe CouchbaseOrm::ActiveRecordCompat do
21
33
  expect(foo.values_at([:compute_age])).to eq([42])
22
34
  end
23
35
  end
36
+
37
+ describe '.primary_key' do
38
+ it 'is always "id"' do
39
+ expect(Foo.primary_key).to eq('id')
40
+ end
41
+ end
42
+
43
+ describe '.base_class?' do
44
+ it 'is always true' do
45
+ expect(Foo.base_class?).to be true
46
+ end
47
+ end
48
+
49
+ describe '.column_names' do
50
+ it 'delegates to .attribute_names' do
51
+ expect(Foo.column_names).to eq(%w[id compute_age])
52
+ end
53
+ end
54
+
55
+ describe '.abstract_class?' do
56
+ it 'is always false' do
57
+ expect(Foo.abstract_class?).to be false
58
+ end
59
+ end
60
+
61
+ describe '.connected?' do
62
+ it 'is always true' do
63
+ expect(Foo.connected?).to be true
64
+ end
65
+ end
66
+
67
+ describe '.table_exists?' do
68
+ it 'is always true' do
69
+ expect(Foo.table_exists?).to be true
70
+ end
71
+ end
72
+
73
+ describe '._reflect_on_association' do
74
+ it 'is always false' do
75
+ expect(Foo._reflect_on_association(:anything)).to be false
76
+ end
77
+ end
78
+
79
+ describe '.type_for_attribute' do
80
+ it 'delegates to .attribute_types' do
81
+ expect(Foo.type_for_attribute('id')).to eq(:string)
82
+ end
83
+ end
84
+
85
+ describe '#_has_attribute?' do
86
+ it 'checks membership in #attribute_names' do
87
+ expect(foo._has_attribute?(:compute_age)).to be true
88
+ expect(foo._has_attribute?(:unknown)).to be false
89
+ end
90
+ end
91
+
92
+ describe '#attribute_for_inspect' do
93
+ it 'inspects the value returned by the named method' do
94
+ expect(foo.attribute_for_inspect(:compute_age)).to eq('42')
95
+ end
96
+ end
24
97
  end
@@ -13,4 +13,41 @@ describe 'CouchbaseOrm::Changeable' do
13
13
  doc = Doc.find(doc.id)
14
14
  expect(doc.changes).to be_empty
15
15
  end
16
+
17
+ describe 'per-attribute dirty methods generated by create_dirty_methods' do
18
+ # `changes_applied` runs the same in-memory bookkeeping create/update
19
+ # triggers after a save (no DB round trip needed to exercise it).
20
+ let(:doc) do
21
+ Doc.new(title: 'initial').tap do |d|
22
+ d.title = 'Test'
23
+ d.changes_applied
24
+ end
25
+ end
26
+
27
+ it 'exposes the change made in the last save' do
28
+ expect(doc.changed?).to be false # cleared by changes_applied
29
+ expect(doc.title_change).to be_nil # no change pending right now
30
+ expect(doc.will_save_change_to_title?).to be false
31
+ expect(doc.title_previously_was).to eq('initial')
32
+ expect(doc.title_before_last_save).to be_nil # only set by a real save, not by changes_applied alone
33
+ expect(doc.title_previously_changed?).to be true
34
+ expect(doc.title_previous_change).to eq(['initial', 'Test'])
35
+ end
36
+
37
+ it 'exposes the change during the save that triggered it, with an explicit from/to' do
38
+ expect(doc.saved_change_to_title?(from: 'initial')).to be true
39
+ expect(doc.saved_change_to_title?(to: 'Test')).to be true
40
+ expect(doc.saved_change_to_title?(from: 'initial', to: 'Test')).to be true
41
+ expect(doc.saved_change_to_title?(from: 'nope')).to be false
42
+ end
43
+
44
+ it 'can flag an attribute as about to change, and reset it back' do
45
+ expect { doc.title_will_change! }.not_to raise_error
46
+ expect { doc.reset_title! }.not_to raise_error
47
+ end
48
+
49
+ it 'falls back to nil for an attribute that was never saved' do
50
+ expect(Doc.new.title_previously_was).to be_nil
51
+ end
52
+ end
16
53
  end
data/spec/n1ql_spec.rb CHANGED
@@ -196,6 +196,25 @@ describe CouchbaseOrm::N1ql do
196
196
  end
197
197
  end
198
198
 
199
+ it "should use adhoc: true by default (no prepared statement plan caching)" do
200
+ expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: true)).and_call_original
201
+ N1QLTest.by_rating_reverse()
202
+ end
203
+
204
+ it "should allow overriding adhoc per call to enable plan caching" do
205
+ expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: false)).and_call_original
206
+ N1QLTest.by_rating_reverse(adhoc: false)
207
+ end
208
+
209
+ it "should respect N1ql.config adhoc setting" do
210
+ default_config = CouchbaseOrm::N1ql.config
211
+ CouchbaseOrm::N1ql.config({ adhoc: false })
212
+ expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: false)).and_call_original
213
+ N1QLTest.by_rating_reverse()
214
+ ensure
215
+ CouchbaseOrm::N1ql.config(default_config)
216
+ end
217
+
199
218
  after(:all) do
200
219
  N1QLTest.delete_all
201
220
  end
@@ -0,0 +1,53 @@
1
+ require 'couchbase-orm/proxies/bucket_proxy'
2
+ require 'couchbase-orm/proxies/n1ql_proxy'
3
+ require 'couchbase-orm/proxies/results_proxy'
4
+
5
+ RSpec.describe CouchbaseOrm::BucketProxy do
6
+ let(:proxyfied) { double('bucket') }
7
+
8
+ it 'raises when asked to proxy nil' do
9
+ expect { CouchbaseOrm::BucketProxy.new(nil) }.to raise_error(ArgumentError)
10
+ end
11
+
12
+ it 'delegates unknown methods to the proxyfied object' do
13
+ allow(proxyfied).to receive(:name).and_return('my_bucket')
14
+ expect(CouchbaseOrm::BucketProxy.new(proxyfied).name).to eq('my_bucket')
15
+ end
16
+
17
+ it 'delegates unknown methods with args, kwargs and a block' do
18
+ allow(proxyfied).to receive(:default_collection).with('a', b: 1).and_yield(:yielded)
19
+ CouchbaseOrm::BucketProxy.new(proxyfied).default_collection('a', b: 1) do |arg|
20
+ expect(arg).to eq(:yielded)
21
+ end
22
+ end
23
+
24
+ it 'wraps #n1ql in a N1qlProxy' do
25
+ allow(proxyfied).to receive(:n1ql).and_return(:the_n1ql)
26
+ expect(CouchbaseOrm::BucketProxy.new(proxyfied).n1ql).to be_a(CouchbaseOrm::N1qlProxy)
27
+ end
28
+
29
+ describe '#view' do
30
+ it 'wraps the result in a ResultsProxy and caches it for the same design/view' do
31
+ allow(proxyfied).to receive(:view).with('design', 'view').and_return([:row1, :row2])
32
+ proxy = CouchbaseOrm::BucketProxy.new(proxyfied)
33
+
34
+ first = proxy.view('design', 'view')
35
+ second = proxy.view('design', 'view')
36
+
37
+ expect(first).to be_a(CouchbaseOrm::ResultsProxy)
38
+ expect(second).to be(first)
39
+ expect(proxyfied).to have_received(:view).once
40
+ end
41
+
42
+ it 'requeries when the design/view pair changes' do
43
+ allow(proxyfied).to receive(:view).with('design', 'view').and_return([:row1])
44
+ allow(proxyfied).to receive(:view).with('design', 'other').and_return([:row2])
45
+ proxy = CouchbaseOrm::BucketProxy.new(proxyfied)
46
+
47
+ proxy.view('design', 'view')
48
+ proxy.view('design', 'other')
49
+
50
+ expect(proxyfied).to have_received(:view).twice
51
+ end
52
+ end
53
+ end
@@ -501,5 +501,38 @@ describe CouchbaseOrm::Relation do
501
501
  end
502
502
  end
503
503
  end
504
+
505
+ it "should use adhoc: true by default (no prepared statement plan caching)" do
506
+ expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: true)).and_call_original
507
+ RelationModel.where(active: true).ids
508
+ end
509
+
510
+ describe "adhoc option via with" do
511
+ it "should return a relation when calling with(adhoc:)" do
512
+ expect(RelationModel.all.with(adhoc: false)).to be_a(CouchbaseOrm::Relation::CouchbaseOrm_Relation)
513
+ end
514
+
515
+ it "should pass adhoc: false to query options when set on the relation" do
516
+ expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: false)).and_call_original
517
+ RelationModel.where(active: true).with(adhoc: false).ids
518
+ end
519
+
520
+ it "should override N1ql.config adhoc when set on the relation" do
521
+ default_config = CouchbaseOrm::N1ql.config
522
+ CouchbaseOrm::N1ql.config(adhoc: false)
523
+ expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: true)).and_call_original
524
+ RelationModel.where(active: true).with(adhoc: true).ids
525
+ ensure
526
+ CouchbaseOrm::N1ql.config(default_config)
527
+ end
528
+
529
+ it "should be chainable with other relation methods" do
530
+ m1 = RelationModel.create!(active: true, age: 10)
531
+ _m2 = RelationModel.create!(active: false, age: 20)
532
+ expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: false)).and_call_original
533
+ result = RelationModel.where(active: true).order(:age).with(adhoc: false).to_a
534
+ expect(result).to match_array([m1])
535
+ end
536
+ end
504
537
  end
505
538
 
@@ -25,6 +25,16 @@ describe CouchbaseOrm::Types::Encrypted do
25
25
  let(:the_secret) { "\x17`\x1F\xEE\el\xE0\x9F<\x94\xFE\x8A.\x1A\x92\xB9\xC3@\x86\x9Cp\xBEl\x86\x0E\x8CJ\tB\x97*U)\x96\x06\xA3\xE9\x84\xA6xW%\xDCT\x8C^\xEA\t\xC7\xD8\xFC\xF1\xD3\xD3\xE2\xEA\x89\xCBuUs\xB3\xFF'W>\xDE\x9CP\xA9\xDE%\xA2\xDE\x11\xFD\b\x9C\xD4\x87J,\x91\x02f\x16R\xDE\x908\x05\x1C\xF9\xDF{\x0F\xB3e\xB2\xB2\x96\xD7\xCC\x16As\xD3I\x02w\xE0\x8FL\xC6S\xEFP\xAC\x15\e^\xC4!\x15\"KF1\x17\x06\xA0N\x00\x18\xBA\x87\xEA?H\xD4<\xB5\xBCV\xB50\fc\xC9F\"\xF0B\eg%\x8E\x88\xD0\x9Bc\xE4\x93\t\x98\xC8\x87\xCB4]\xD9K\xA3\xDF\x13Q\xC0T\xCA\x91;\b\x9Cp\xE0\x7FR h\xDA\xB7\xD5\x869\f\xCA\x80\x802\x19\x19\xDD\x9DO\xAE}\xCA\eX\xA3\xA8\xBE\xE1\xBCW0g\x19@5n\r\xD8\xF3\x05\x7F4\x9CI\x1F3\xC0\xBDQJyG\v\xED!s\xD5\xD0&\xC1\x1A\xBC\x17\xFD\x9Cd\xB5\xAF\xB6U\x8A" }
26
26
  let(:base64_secret) { Base64.strict_encode64(the_secret) }
27
27
 
28
+ describe "#serialize" do
29
+ it "returns nil for a nil value" do
30
+ expect(CouchbaseOrm::Types::Encrypted.new.serialize(nil)).to be_nil
31
+ end
32
+
33
+ it "returns the value unchanged otherwise (wrapping happens in Encrypt#encode_encrypted_attributes)" do
34
+ expect(CouchbaseOrm::Types::Encrypted.new.serialize(base64_secret)).to eq(base64_secret)
35
+ end
36
+ end
37
+
28
38
  it "prefix attribute on serialization" do
29
39
  obj = TypeEncryptedTest.new(secret: base64_secret, secret2: "a secret")
30
40
  expect_serialized_secret(obj)
@@ -50,6 +60,16 @@ describe CouchbaseOrm::Types::Encrypted do
50
60
  expect(obj.as_json["secret2"]).to eq "a secret"
51
61
  end
52
62
 
63
+ it "raises when serializing a non-String value for an encrypted attribute" do
64
+ obj = TypeEncryptedTest.new(secret: 12345)
65
+ expect { obj.send(:serialized_attributes) }.to raise_error(/Can not serialize value/)
66
+ end
67
+
68
+ it "raises from #as_json when a non-String value is held by an encrypted attribute" do
69
+ obj = TypeEncryptedTest.new(secret: 12345)
70
+ expect { obj.as_json }.to raise_error(/Can not serialize value/)
71
+ end
72
+
53
73
  it "prefix with custom algo" do
54
74
  obj = SpecificAlgoTest.new(secret: base64_secret)
55
75
  expect(obj.send(:serialized_attributes)["encrypted$secret"]).to eq({alg:"3DES", ciphertext: base64_secret})
@@ -18,6 +18,18 @@ class TypeNestedTest < CouchbaseOrm::Base
18
18
  end
19
19
 
20
20
  describe CouchbaseOrm::Types::Nested do
21
+ describe "#serialize" do
22
+ it "builds an instance from a raw Hash before serializing it, filtering ignored properties" do
23
+ # `serialize` is normally called with an already-built NestedDocument
24
+ # instance (see the specs below); this exercises its Hash branch
25
+ # directly, using string keys to match the shape a real document
26
+ # (and thus `ignored_properties`, which stores string names) has.
27
+ type = CouchbaseOrm::Types::Nested.new(type: SubTypeWithIgnoredProperties)
28
+ serialized = type.serialize("name" => "Nested", "value" => "Valid", "deprecated_property" => "should be dropped")
29
+ expect(serialized).to eq("name" => "Nested", "value" => "Valid")
30
+ end
31
+ end
32
+
21
33
  it "should be able to store and retrieve a nested object" do
22
34
  obj = TypeNestedTest.new
23
35
  obj.main = SubTypeTest.new
@@ -255,4 +267,84 @@ describe CouchbaseOrm::Types::Nested do
255
267
  expect(parent.nested.value).to eq("Parent Value")
256
268
  end
257
269
  end
270
+
271
+ describe "raise_on_unknown_attributes" do
272
+ class SubTypeWithUnknownAttributesAllowed < CouchbaseOrm::NestedDocument
273
+ self.raise_on_unknown_attributes = false
274
+ attribute :label, :string
275
+ end
276
+
277
+ class SubTypeWithDefaultUnknownAttributes < CouchbaseOrm::NestedDocument
278
+ attribute :label, :string
279
+ end
280
+
281
+ class ParentWithNestedUnknownAttributesAllowed < CouchbaseOrm::Base
282
+ self.raise_on_unknown_attributes = false
283
+ attribute :title, :string
284
+ attribute :main, :nested, type: SubTypeWithUnknownAttributesAllowed
285
+ attribute :others, :array, type: SubTypeWithUnknownAttributesAllowed
286
+ end
287
+
288
+ class ParentWithStrictNestedUnknownAttributes < CouchbaseOrm::Base
289
+ self.raise_on_unknown_attributes = false
290
+ attribute :title, :string
291
+ attribute :main, :nested, type: SubTypeWithDefaultUnknownAttributes
292
+ end
293
+
294
+ it "tolerates an unknown key in a nested document assigned as a Hash, via Types::Nested#cast" do
295
+ # This is the path the actual document-loading code takes; assigning an
296
+ # already-built NestedDocument *instance* (as opposed to a Hash) never
297
+ # reaches Types::Nested#cast at all.
298
+ parent = ParentWithNestedUnknownAttributesAllowed.new(title: "Parent")
299
+ parent.main = SubTypeWithUnknownAttributesAllowed.new(label: "Main")
300
+ parent.save!
301
+
302
+ doc_id = parent.id
303
+ raw_doc = ParentWithNestedUnknownAttributesAllowed.bucket.default_collection.get(doc_id).content
304
+ raw_doc["main"]["legacy"] = "should be ignored"
305
+ ParentWithNestedUnknownAttributesAllowed.bucket.default_collection.replace(doc_id, raw_doc)
306
+
307
+ parent.reload
308
+ expect(parent.main.label).to eq("Main")
309
+ expect(parent.main.attributes.keys).not_to include("legacy")
310
+
311
+ # Same assertion via `find` - a different code path (the GetResult
312
+ # branch of Document#initialize) than `reload`.
313
+ reloaded = ParentWithNestedUnknownAttributesAllowed.find(doc_id)
314
+ expect(reloaded.main.label).to eq("Main")
315
+ expect(reloaded.main.attributes.keys).not_to include("legacy")
316
+ end
317
+
318
+ it "tolerates an unknown key in a nested document inside a Types::Array, assigned as Hashes" do
319
+ parent = ParentWithNestedUnknownAttributesAllowed.new(title: "Parent")
320
+ parent.others = [
321
+ SubTypeWithUnknownAttributesAllowed.new(label: "First"),
322
+ SubTypeWithUnknownAttributesAllowed.new(label: "Second")
323
+ ]
324
+ parent.save!
325
+
326
+ doc_id = parent.id
327
+ raw_doc = ParentWithNestedUnknownAttributesAllowed.bucket.default_collection.get(doc_id).content
328
+ raw_doc["others"][0]["legacy"] = "should be ignored"
329
+ ParentWithNestedUnknownAttributesAllowed.bucket.default_collection.replace(doc_id, raw_doc)
330
+
331
+ parent.reload
332
+ expect(parent.others.map(&:label)).to eq(["First", "Second"])
333
+ expect(parent.others[0].attributes.keys).not_to include("legacy")
334
+ expect(parent.others[1].attributes.keys).not_to include("legacy")
335
+ end
336
+
337
+ it "is per nested class: a tolerant parent does not make a strict nested document tolerant" do
338
+ parent = ParentWithStrictNestedUnknownAttributes.new(title: "Parent")
339
+ parent.main = SubTypeWithDefaultUnknownAttributes.new(label: "Main")
340
+ parent.save!
341
+
342
+ doc_id = parent.id
343
+ raw_doc = ParentWithStrictNestedUnknownAttributes.bucket.default_collection.get(doc_id).content
344
+ raw_doc["main"]["legacy"] = "should raise"
345
+ ParentWithStrictNestedUnknownAttributes.bucket.default_collection.replace(doc_id, raw_doc)
346
+
347
+ expect { parent.reload }.to raise_error(ActiveModel::UnknownAttributeError)
348
+ end
349
+ end
258
350
  end