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.
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true, encoding: ASCII-8BIT
2
+
3
+ require File.expand_path("../support", __FILE__)
4
+
5
+ # A minimal stand-in for ActionController::Parameters (each_pair/each/except,
6
+ # no #to_hash). Real Parameters would work too, but pulling in actionpack
7
+ # here would make this spec's outcome depend on whatever version bundler
8
+ # happens to resolve it to; what actually matters is that the filter is
9
+ # guarded by `respond_to?(:each_pair)`, not `is_a?(Hash)`.
10
+ #
11
+ # `each` is required too, not just `each_pair`: ActiveModel::AttributeAssignment's
12
+ # own `_assign_attributes` (which our `super` eventually reaches) iterates via
13
+ # `.each` on ActiveModel < 8.0, and via `.each_pair` on 8.0+.
14
+ class EachPairOnly
15
+ def initialize(hash)
16
+ @hash = hash
17
+ end
18
+
19
+ def each_pair(&block)
20
+ @hash.each_pair(&block)
21
+ end
22
+ alias_method :each, :each_pair
23
+
24
+ def except(*keys)
25
+ EachPairOnly.new(@hash.except(*keys))
26
+ end
27
+
28
+ def empty?
29
+ @hash.empty?
30
+ end
31
+ end
32
+
33
+ class UnknownAttrsStrict < CouchbaseOrm::Base
34
+ attribute :name, :string
35
+ end
36
+
37
+ class UnknownAttrsTolerant < CouchbaseOrm::Base
38
+ self.raise_on_unknown_attributes = false
39
+ attribute :name, :string
40
+ attribute :secret, :encrypted
41
+ end
42
+
43
+ class UnknownAttrsTolerantChild < UnknownAttrsTolerant
44
+ end
45
+
46
+ class UnknownAttrsBoth < CouchbaseOrm::Base
47
+ self.raise_on_unknown_attributes = false
48
+ self.ignored_properties = [:legacy_ignored]
49
+ attribute :name, :string
50
+ end
51
+
52
+ class UnknownAttrsAssocParent < CouchbaseOrm::Base
53
+ attribute :name, :string
54
+ end
55
+
56
+ class UnknownAttrsAssocChild < CouchbaseOrm::Base
57
+ self.raise_on_unknown_attributes = false
58
+ attribute :name, :string
59
+ belongs_to :unknown_attrs_assoc_parent, dependent: :destroy
60
+ end
61
+
62
+ RSpec.describe "raise_on_unknown_attributes" do
63
+ # No public reset API on purpose (it would be test-only surface on
64
+ # production code) - reach into the module's own warn-once tracking
65
+ # directly so these examples don't depend on run order.
66
+ before { CouchbaseOrm::UnknownAttributes.instance_variable_get(:@warned).clear }
67
+
68
+ describe "defaults" do
69
+ it "defaults to true" do
70
+ expect(CouchbaseOrm::Base.raise_on_unknown_attributes).to be true
71
+ expect(UnknownAttrsStrict.raise_on_unknown_attributes).to be true
72
+ end
73
+
74
+ it "still raises ActiveModel::UnknownAttributeError when true" do
75
+ expect { UnknownAttrsStrict.new(name: 'bob', nope: 'x') }.to raise_error(ActiveModel::UnknownAttributeError)
76
+ model = UnknownAttrsStrict.new(name: 'bob')
77
+ expect { model.assign_attributes(nope: 'x') }.to raise_error(ActiveModel::UnknownAttributeError)
78
+ end
79
+ end
80
+
81
+ describe "inheritance" do
82
+ it "is inherited by subclasses, unlike ignored_properties" do
83
+ expect(UnknownAttrsTolerantChild.raise_on_unknown_attributes).to be false
84
+ end
85
+
86
+ it "does not leak from a subclass back to its parent or to Base" do
87
+ expect(UnknownAttrsTolerant.raise_on_unknown_attributes).to be false
88
+ expect(CouchbaseOrm::Base.raise_on_unknown_attributes).to be true
89
+ end
90
+ end
91
+
92
+ describe "association writers are not treated as unknown attributes" do
93
+ it "still resolves belongs_to via its writer, not via attribute_names" do
94
+ parent = UnknownAttrsAssocParent.create!(name: 'parent')
95
+ child = UnknownAttrsAssocChild.new(name: 'child', unknown_attrs_assoc_parent: parent, really_unknown: 'x')
96
+
97
+ expect(child.unknown_attrs_assoc_parent_id).to eq(parent.id)
98
+ expect(child.respond_to?(:really_unknown)).to be false
99
+ end
100
+ end
101
+
102
+ describe "given a document with an unknown property" do
103
+ let(:doc_id) { "unknown_attrs_tolerant_#{SecureRandom.hex}" }
104
+ let(:document_properties) do
105
+ {
106
+ 'type' => UnknownAttrsTolerant.design_document,
107
+ 'name' => 'Alice',
108
+ 'server_encrypted_content' => 'not yet known to this code'
109
+ }
110
+ end
111
+
112
+ before { UnknownAttrsTolerant.bucket.default_collection.upsert doc_id, document_properties }
113
+ after { UnknownAttrsTolerant.bucket.default_collection.remove doc_id }
114
+
115
+ it "does not raise on find (the INC-5405 path) and drops the unknown key" do
116
+ model = UnknownAttrsTolerant.find(doc_id)
117
+ expect(model.name).to eq('Alice')
118
+ expect(model.attributes.keys).not_to include('server_encrypted_content')
119
+ end
120
+
121
+ it "raises on find when raise_on_unknown_attributes is true" do
122
+ document_properties['type'] = UnknownAttrsStrict.design_document
123
+ UnknownAttrsStrict.bucket.default_collection.upsert doc_id, document_properties
124
+ expect { UnknownAttrsStrict.find(doc_id) }.to raise_error(ActiveModel::UnknownAttributeError)
125
+ end
126
+
127
+ it "does not raise on reload" do
128
+ model = UnknownAttrsTolerant.find(doc_id)
129
+ expect { model.reload }.not_to raise_error
130
+ expect(model.attributes.keys).not_to include('server_encrypted_content')
131
+ end
132
+
133
+ it "logs a warning exactly once per (class, key), and debug on every call" do
134
+ allow(CouchbaseOrm.logger).to receive(:debug).and_call_original
135
+ expect(CouchbaseOrm.logger).to receive(:warn).once.with(a_string_including('server_encrypted_content'))
136
+
137
+ UnknownAttrsTolerant.find(doc_id)
138
+ UnknownAttrsTolerant.find(doc_id)
139
+ end
140
+
141
+ it "emits no warning at all when raise_on_unknown_attributes is true" do
142
+ document_properties['type'] = UnknownAttrsStrict.design_document
143
+ UnknownAttrsStrict.bucket.default_collection.upsert doc_id, document_properties
144
+ expect(CouchbaseOrm.logger).not_to receive(:warn)
145
+ expect { UnknownAttrsStrict.find(doc_id) }.to raise_error(ActiveModel::UnknownAttributeError)
146
+ end
147
+
148
+ it "drops the unknown key from the stored document on the next save (same trade-off as ignored_properties)" do
149
+ model = UnknownAttrsTolerant.find(doc_id)
150
+ model.name = 'Alice Updated'
151
+ expect { model.save! }.to change {
152
+ UnknownAttrsTolerant.bucket.default_collection.get(doc_id).content.keys.sort
153
+ }.from(%w[name server_encrypted_content type]).to(%w[name type])
154
+ end
155
+ end
156
+
157
+ describe "interaction with encrypted$-prefixed keys" do
158
+ it "tolerates an unknown key that only happens to carry the encrypted$ prefix" do
159
+ doc_id = "unknown_attrs_tolerant_enc_#{SecureRandom.hex}"
160
+ UnknownAttrsTolerant.bucket.default_collection.upsert doc_id, {
161
+ 'type' => UnknownAttrsTolerant.design_document,
162
+ 'name' => 'Bob',
163
+ 'encrypted$tanker_encrypted_content' => { 'alg' => 'tanker', 'ciphertext' => 'legacy' }
164
+ }
165
+
166
+ model = UnknownAttrsTolerant.find(doc_id)
167
+ expect(model.name).to eq('Bob')
168
+ expect(model.attributes.keys).not_to include('tanker_encrypted_content', 'encrypted$tanker_encrypted_content')
169
+ ensure
170
+ UnknownAttrsTolerant.bucket.default_collection.remove doc_id
171
+ end
172
+ end
173
+
174
+ describe "combined with ignored_properties" do
175
+ let(:doc_id) { "unknown_attrs_both_#{SecureRandom.hex}" }
176
+
177
+ before do
178
+ UnknownAttrsBoth.bucket.default_collection.upsert doc_id, {
179
+ 'type' => UnknownAttrsBoth.design_document,
180
+ 'name' => 'Carol',
181
+ 'legacy_ignored' => 'handled by ignored_properties',
182
+ 'other_unknown' => 'handled by the flag'
183
+ }
184
+ end
185
+ after { UnknownAttrsBoth.bucket.default_collection.remove doc_id }
186
+
187
+ it "tolerates both without raising, and only warns about the one the flag handled" do
188
+ expect(CouchbaseOrm.logger).to receive(:warn).once.with(a_string_including('other_unknown'))
189
+ model = UnknownAttrsBoth.find(doc_id)
190
+ expect(model.name).to eq('Carol')
191
+ expect(model.attributes.keys).not_to include('legacy_ignored', 'other_unknown')
192
+ end
193
+ end
194
+
195
+ describe "duck-typed, non-Hash input (e.g. ActionController::Parameters)" do
196
+ it "filters through a respond_to?(:each_pair) object just like a Hash" do
197
+ # Calling assign_attributes directly (rather than .new) matters here:
198
+ # Document#initialize's generic branch runs every non-GetResult,
199
+ # non-CouchbaseOrm::Base argument through Kernel#Hash(), which would
200
+ # coerce a real permitted ActionController::Parameters (it defines
201
+ # #to_hash) before assign_attributes ever saw it - masking exactly
202
+ # the guard this test means to exercise.
203
+ model = UnknownAttrsTolerant.new(name: 'Dave')
204
+ model.assign_attributes(EachPairOnly.new(name: 'Dave Updated', nope: 'x'))
205
+ expect(model.name).to eq('Dave Updated')
206
+ expect(model.respond_to?(:nope)).to be false
207
+ end
208
+
209
+ it "still raises for a non-Hash, non-each_pair argument, same as before this feature existed" do
210
+ # Persistence#assign_attributes (unrelated to raise_on_unknown_attributes,
211
+ # pre-existing on CouchbaseOrm::Base) calls `hash.except("type")`
212
+ # unconditionally, so on Base this is a NoMethodError rather than the
213
+ # ArgumentError plain ActiveModel would raise. Either way, it must not
214
+ # be silently swallowed by the unknown-attributes filter.
215
+ expect { UnknownAttrsTolerant.new.assign_attributes(Object.new) }.to raise_error(StandardError)
216
+ end
217
+ end
218
+
219
+ describe "attributes=" do
220
+ it "goes through the same filtering as assign_attributes" do
221
+ model = UnknownAttrsTolerant.new(name: 'Eve')
222
+ model.attributes = { name: 'Eve Updated', nope: 'x' }
223
+ expect(model.name).to eq('Eve Updated')
224
+ expect(model.respond_to?(:nope)).to be false
225
+ end
226
+ end
227
+ end
@@ -1,3 +1,4 @@
1
+ require 'couchbase-orm'
1
2
  require 'couchbase-orm/utilities/ignored_properties'
2
3
 
3
4
  class DummyClass
@@ -17,4 +18,20 @@ RSpec.describe CouchbaseOrm::IgnoredProperties do
17
18
  expect(DummyClass2.ignored_properties).to be_empty
18
19
  end
19
20
  end
21
+
22
+ describe '#ignored_properties' do
23
+ # A fresh class per example, so the deprecated-setter test below can't
24
+ # leak state into (or depend on run order with) any other example.
25
+ let(:klass) { Class.new { extend CouchbaseOrm::IgnoredProperties } }
26
+
27
+ it 'defaults to an empty array' do
28
+ expect(klass.ignored_properties).to eq([])
29
+ end
30
+
31
+ it 'is deprecated as a setter and warns when called with arguments' do
32
+ expect(CouchbaseOrm.logger).to receive(:warn).with(a_string_including('deprecated'))
33
+ klass.ignored_properties(:legacy_property)
34
+ expect(klass.ignored_properties).to eq(['legacy_property'])
35
+ end
36
+ end
20
37
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: couchbase-orm
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen von Takach
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2026-06-30 00:00:00.000000000 Z
14
+ date: 2026-09-29 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activemodel
@@ -216,6 +216,7 @@ extensions: []
216
216
  extra_rdoc_files: []
217
217
  files:
218
218
  - ".github/workflows/deploy.yml"
219
+ - ".github/workflows/release.yml"
219
220
  - ".github/workflows/test.yml"
220
221
  - ".gitignore"
221
222
  - ".rspec"
@@ -299,6 +300,7 @@ files:
299
300
  - lib/couchbase-orm/types/encrypted.rb
300
301
  - lib/couchbase-orm/types/nested.rb
301
302
  - lib/couchbase-orm/types/timestamp.rb
303
+ - lib/couchbase-orm/unknown_attributes.rb
302
304
  - lib/couchbase-orm/utilities/ensure_unique.rb
303
305
  - lib/couchbase-orm/utilities/enum.rb
304
306
  - lib/couchbase-orm/utilities/has_many.rb
@@ -336,6 +338,7 @@ files:
336
338
  - spec/json_schema_spec.rb
337
339
  - spec/n1ql_spec.rb
338
340
  - spec/persistence_spec.rb
341
+ - spec/proxies/bucket_proxy_spec.rb
339
342
  - spec/relation_nested_spec.rb
340
343
  - spec/relation_spec.rb
341
344
  - spec/support.rb
@@ -343,6 +346,7 @@ files:
343
346
  - spec/type_encrypted_spec.rb
344
347
  - spec/type_nested_spec.rb
345
348
  - spec/type_spec.rb
349
+ - spec/unknown_attributes_spec.rb
346
350
  - spec/utilities/ignored_properties_spec.rb
347
351
  - spec/utilities/properties_always_exists_in_document_spec.rb
348
352
  - spec/views_spec.rb
@@ -362,7 +366,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
362
366
  requirements:
363
367
  - - ">="
364
368
  - !ruby/object:Gem::Version
365
- version: 3.1.0
369
+ version: 3.3.0
366
370
  required_rubygems_version: !ruby/object:Gem::Requirement
367
371
  requirements:
368
372
  - - ">="
@@ -396,6 +400,7 @@ test_files:
396
400
  - spec/json_schema_spec.rb
397
401
  - spec/n1ql_spec.rb
398
402
  - spec/persistence_spec.rb
403
+ - spec/proxies/bucket_proxy_spec.rb
399
404
  - spec/relation_nested_spec.rb
400
405
  - spec/relation_spec.rb
401
406
  - spec/support.rb
@@ -403,6 +408,7 @@ test_files:
403
408
  - spec/type_encrypted_spec.rb
404
409
  - spec/type_nested_spec.rb
405
410
  - spec/type_spec.rb
411
+ - spec/unknown_attributes_spec.rb
406
412
  - spec/utilities/ignored_properties_spec.rb
407
413
  - spec/utilities/properties_always_exists_in_document_spec.rb
408
414
  - spec/views_spec.rb