mongoid 9.0.10 → 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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -3
  3. data/Rakefile +31 -0
  4. data/lib/config/locales/en.yml +45 -0
  5. data/lib/mongoid/association/depending.rb +8 -4
  6. data/lib/mongoid/association/nested/many.rb +38 -3
  7. data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
  8. data/lib/mongoid/association/referenced/auto_save.rb +48 -6
  9. data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
  10. data/lib/mongoid/association/relatable.rb +17 -0
  11. data/lib/mongoid/collection_configurable.rb +8 -0
  12. data/lib/mongoid/config/defaults.rb +9 -5
  13. data/lib/mongoid/config/encryption.rb +36 -18
  14. data/lib/mongoid/config.rb +72 -0
  15. data/lib/mongoid/contextual/aggregable/memory.rb +5 -2
  16. data/lib/mongoid/contextual/memory.rb +18 -7
  17. data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
  18. data/lib/mongoid/criteria/queryable/selectable.rb +109 -0
  19. data/lib/mongoid/encryptable.rb +46 -0
  20. data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
  21. data/lib/mongoid/errors/no_encryption_schema.rb +28 -0
  22. data/lib/mongoid/errors.rb +2 -0
  23. data/lib/mongoid/field_readable.rb +70 -0
  24. data/lib/mongoid/indexable.rb +39 -27
  25. data/lib/mongoid/matchable.rb +6 -1
  26. data/lib/mongoid/matcher/eq_impl_with_regexp.rb +4 -9
  27. data/lib/mongoid/matcher/regex.rb +9 -13
  28. data/lib/mongoid/matcher/regexp_budget.rb +383 -0
  29. data/lib/mongoid/matcher.rb +1 -0
  30. data/lib/mongoid/persistence_context.rb +35 -0
  31. data/lib/mongoid/search_indexable.rb +14 -7
  32. data/lib/mongoid/tasks/database.rb +23 -9
  33. data/lib/mongoid/threaded.rb +37 -0
  34. data/lib/mongoid/version.rb +1 -1
  35. data/lib/mongoid/warnings.rb +4 -0
  36. data/spec/integration/app_spec.rb +201 -36
  37. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
  38. data/spec/integration/dots_and_dollars_spec.rb +12 -2
  39. data/spec/integration/encryption_spec.rb +149 -0
  40. data/spec/integration/matcher_operator_data/regex.yml +21 -0
  41. data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
  42. data/spec/integration/query_operator_guard_spec.rb +89 -0
  43. data/spec/mongoid/association/auto_save_spec.rb +98 -0
  44. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
  45. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
  46. data/spec/mongoid/attributes/nested_spec.rb +250 -30
  47. data/spec/mongoid/config/defaults_spec.rb +18 -0
  48. data/spec/mongoid/config/encryption_spec.rb +228 -0
  49. data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
  50. data/spec/mongoid/contextual/memory_spec.rb +177 -0
  51. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +2 -0
  52. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +200 -0
  53. data/spec/mongoid/criteria_spec.rb +2 -0
  54. data/spec/mongoid/encryptable_spec.rb +61 -0
  55. data/spec/mongoid/errors/document_not_found_spec.rb +10 -3
  56. data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
  57. data/spec/mongoid/tasks/database_spec.rb +18 -0
  58. data/spec/support/crypt/models.rb +157 -0
  59. metadata +28 -2
@@ -37,37 +37,34 @@ describe 'Mongoid application tests' do
37
37
  FileUtils.mkdir_p(TMP_BASE)
38
38
  end
39
39
 
40
- context 'demo application' do
40
+ context 'generated application' do
41
41
  context 'sinatra' do
42
42
  it 'runs' do
43
- skip 'https://jira.mongodb.org/browse/MONGOID-5826'
44
-
45
- clone_application(
46
- 'https://github.com/mongoid/mongoid-demo',
47
- subdir: 'sinatra-minimal',
48
- ) do
49
-
43
+ create_sinatra_app('mongoid-sinatra-test') do
50
44
  # JRuby needs a long timeout
51
45
  start_app(%w(bundle exec ruby app.rb), 4567, 40) do |port|
52
46
  uri = URI.parse('http://localhost:4567/posts')
53
47
  resp = JSON.parse(uri.open.read)
54
48
 
55
49
  resp.should == []
56
-
57
50
  end
58
51
  end
59
52
  end
60
53
  end
61
54
 
62
55
  context 'rails-api' do
63
- it 'runs' do
64
- skip 'https://jira.mongodb.org/browse/MONGOID-5826'
65
-
66
- clone_application(
67
- 'https://github.com/mongoid/mongoid-demo',
68
- subdir: 'rails-api',
69
- ) do
56
+ before(:all) do
57
+ # Rails 6.0/6.1 have Logger issues on Ruby 3.1+
58
+ # Rails < 7.1 have concurrent-ruby issues on Ruby 4.0+
59
+ if RUBY_VERSION >= '4.0' && SpecConfig.instance.rails_version < '7.1'
60
+ skip 'Rails < 7.1 is not compatible with Ruby 4.0+. Set RAILS=7.1 or higher.'
61
+ elsif RUBY_VERSION >= '3.1' && SpecConfig.instance.rails_version < '6.1'
62
+ skip 'Rails < 6.1 is not compatible with Ruby 3.1+. Set RAILS=6.1 or higher.'
63
+ end
64
+ end
70
65
 
66
+ it 'runs' do
67
+ create_rails_api_app('mongoid-rails-api-test') do
71
68
  # JRuby needs a long timeout
72
69
  start_app(%w(bundle exec rails s), 3000, 50) do |port|
73
70
  uri = URI.parse('http://localhost:3000/posts')
@@ -114,7 +111,7 @@ describe 'Mongoid application tests' do
114
111
 
115
112
  Dir.chdir(TMP_BASE) do
116
113
  FileUtils.rm_rf(name)
117
- check_call(insert_rails_gem_version(%W(rails new #{name} --skip-spring --skip-active-record)), env: clean_env)
114
+ check_call(insert_rails_gem_version(%W(rails new #{name} --skip-spring --skip-active-record)), env: rails_env)
118
115
 
119
116
  Dir.chdir(name) do
120
117
  adjust_rails_defaults
@@ -126,10 +123,149 @@ describe 'Mongoid application tests' do
126
123
  end
127
124
  end
128
125
 
126
+ def create_sinatra_app(name)
127
+ Dir.chdir(TMP_BASE) do
128
+ FileUtils.rm_rf(name)
129
+ FileUtils.mkdir_p(name)
130
+
131
+ Dir.chdir(name) do
132
+ # Create minimal Sinatra app with Post model
133
+ File.open('app.rb', 'w') do |f|
134
+ f.write(<<~RUBY)
135
+ require 'sinatra'
136
+ require 'mongoid'
137
+ require 'json'
138
+
139
+ Mongoid.load!('config/mongoid.yml')
140
+
141
+ class Post
142
+ include Mongoid::Document
143
+ field :title, type: String
144
+ end
145
+
146
+ get '/posts' do
147
+ content_type :json
148
+ Post.all.to_json
149
+ end
150
+ RUBY
151
+ end
152
+
153
+ # Create Gemfile
154
+ File.open('Gemfile', 'w') do |f|
155
+ f.write(<<~RUBY)
156
+ source 'https://rubygems.org'
157
+ gem 'sinatra'
158
+ gem 'rackup'
159
+ gem 'mongoid', path: '#{File.expand_path(BASE)}'
160
+ gem 'puma'
161
+ RUBY
162
+ end
163
+
164
+ FileUtils.mkdir_p('config')
165
+ write_mongoid_yml
166
+ check_call(%w(bundle install), env: clean_env)
167
+
168
+ yield
169
+ end
170
+ end
171
+ end
172
+
173
+ def create_rails_api_app(name)
174
+ install_rails
175
+
176
+ Dir.chdir(TMP_BASE) do
177
+ FileUtils.rm_rf(name)
178
+ check_call(insert_rails_gem_version(%W(rails new #{name} --api --skip-spring --skip-active-record)), env: rails_env)
179
+
180
+ Dir.chdir(name) do
181
+ adjust_rails_defaults
182
+ adjust_app_gemfile
183
+
184
+ # Create Post model
185
+ File.open('app/models/post.rb', 'w') do |f|
186
+ f.write(<<~RUBY)
187
+ class Post
188
+ include Mongoid::Document
189
+ field :title, type: String
190
+ end
191
+ RUBY
192
+ end
193
+
194
+ # Create PostsController
195
+ File.open('app/controllers/posts_controller.rb', 'w') do |f|
196
+ f.write(<<~RUBY)
197
+ class PostsController < ApplicationController
198
+ def index
199
+ render json: Post.all
200
+ end
201
+ end
202
+ RUBY
203
+ end
204
+
205
+ # Add route
206
+ routes_content = File.read('config/routes.rb')
207
+ routes_content.sub!(/Rails\.application\.routes\.draw do\n/,
208
+ "Rails.application.routes.draw do\n resources :posts, only: [:index]\n")
209
+ File.open('config/routes.rb', 'w') { |f| f.write(routes_content) }
210
+
211
+ write_mongoid_yml
212
+ check_call(%w(bundle install), env: clean_env)
213
+
214
+ yield
215
+ end
216
+ end
217
+ end
218
+
219
+ def create_rails_rake_test_app(name)
220
+ install_rails
221
+
222
+ Dir.chdir(TMP_BASE) do
223
+ FileUtils.rm_rf(name)
224
+ check_call(insert_rails_gem_version(%W(rails new #{name} --api --skip-spring --skip-active-record)), env: rails_env)
225
+
226
+ Dir.chdir(name) do
227
+ adjust_rails_defaults
228
+ adjust_app_gemfile
229
+
230
+ # Create Post model with index
231
+ File.open('app/models/post.rb', 'w') do |f|
232
+ f.write(<<~RUBY)
233
+ class Post
234
+ include Mongoid::Document
235
+ include Mongoid::Timestamps
236
+ field :subject, type: String
237
+ field :message, type: String
238
+
239
+ index subject: 1
240
+ end
241
+ RUBY
242
+ end
243
+
244
+ # Create Comment model
245
+ File.open('app/models/comment.rb', 'w') do |f|
246
+ f.write(<<~RUBY)
247
+ class Comment
248
+ include Mongoid::Document
249
+ include Mongoid::Timestamps
250
+ belongs_to :post
251
+ end
252
+ RUBY
253
+ end
254
+
255
+ write_mongoid_yml
256
+ check_call(%w(bundle install), env: clean_env)
257
+ end
258
+ end
259
+ end
260
+
129
261
  context 'new application - rails' do
130
262
  before(:all) do
131
- if SpecConfig.instance.rails_version < '7.1'
132
- skip '`rails new` with rails < 7.1 fails because modern concurrent-ruby removed logger dependency'
263
+ # Rails 6.0/6.1 have Logger issues on Ruby 3.1+
264
+ # Rails < 7.1 have concurrent-ruby issues on Ruby 4.0+
265
+ if RUBY_VERSION >= '4.0' && SpecConfig.instance.rails_version < '7.1'
266
+ skip 'Rails < 7.1 is not compatible with Ruby 4.0+. Set RAILS=7.1 or higher.'
267
+ elsif RUBY_VERSION >= '3.1' && SpecConfig.instance.rails_version < '6.1'
268
+ skip 'Rails < 6.1 is not compatible with Ruby 3.1+. Set RAILS=6.1 or higher.'
133
269
  end
134
270
  end
135
271
 
@@ -186,17 +322,31 @@ describe 'Mongoid application tests' do
186
322
  if (rails_version = SpecConfig.instance.rails_version) == 'master'
187
323
  else
188
324
  check_call(%w(gem list))
325
+
326
+ # Rails 6.0 and 6.1 need logger gem on Ruby 2.7+ due to stdlib changes
327
+ if rails_version.to_f < 7.0 && RUBY_VERSION >= '2.7'
328
+ check_call(%w(gem install logger --no-document))
329
+ end
330
+
189
331
  check_call(%w(gem install rails --no-document --force -v) + ["~> #{rails_version}.0"])
190
332
  end
191
333
  end
192
334
 
193
- context 'local test applications' do
335
+ context 'generated test applications' do
336
+ before(:all) do
337
+ # Rails 6.0/6.1 have Logger issues on Ruby 3.1+
338
+ # Rails < 7.1 have concurrent-ruby issues on Ruby 4.0+
339
+ if RUBY_VERSION >= '4.0' && SpecConfig.instance.rails_version < '7.1'
340
+ skip 'Rails < 7.1 is not compatible with Ruby 4.0+. Set RAILS=7.1 or higher.'
341
+ elsif RUBY_VERSION >= '3.1' && SpecConfig.instance.rails_version < '6.1'
342
+ skip 'Rails < 6.1 is not compatible with Ruby 3.1+. Set RAILS=6.1 or higher.'
343
+ end
344
+ end
345
+
194
346
  let(:client) { Mongoid.default_client }
195
347
 
196
348
  describe 'create_indexes rake task' do
197
349
 
198
- APP_PATH = File.join(File.dirname(__FILE__), '../../test-apps/rails-api')
199
-
200
350
  %w(development production).each do |rails_env|
201
351
  context "in #{rails_env}" do
202
352
 
@@ -207,20 +357,11 @@ describe 'Mongoid application tests' do
207
357
  clean_env.merge(RAILS_ENV: rails_env, AUTOLOADER: autoloader)
208
358
  end
209
359
 
360
+ let(:app_name) { "mongoid-rake-test-#{rails_env}-#{autoloader}" }
361
+ let(:app_path) { File.join(TMP_BASE, app_name) }
362
+
210
363
  before do
211
- Dir.chdir(APP_PATH) do
212
- remove_bundler_req
213
-
214
- if BSON::Environment.jruby?
215
- # Remove existing Gemfile.lock - see
216
- # https://github.com/rubygems/rubygems/issues/3231
217
- require 'fileutils'
218
- FileUtils.rm_f('Gemfile.lock')
219
- end
220
-
221
- check_call(%w(bundle install), env: env)
222
- write_mongoid_yml
223
- end
364
+ create_rails_rake_test_app(app_name)
224
365
 
225
366
  client['posts'].drop
226
367
  client['posts'].create
@@ -233,7 +374,7 @@ describe 'Mongoid application tests' do
233
374
  index.should be nil
234
375
 
235
376
  check_call(%w(bundle exec rake db:mongoid:create_indexes -t),
236
- cwd: APP_PATH, env: env)
377
+ cwd: app_path, env: env)
237
378
 
238
379
  index = client['posts'].indexes.detect do |index|
239
380
  index['key'] == {'subject' => 1}
@@ -317,6 +458,19 @@ describe 'Mongoid application tests' do
317
458
  line =~ /mongoid/
318
459
  end
319
460
  gemfile_lines << "gem 'mongoid', path: '#{File.expand_path(BASE)}'\n"
461
+
462
+ # json 3.0 removed the quirks_mode and max_nesting keywords that
463
+ # ActiveSupport::JSON (every 7.x) still passes to JSON.generate and
464
+ # JSON.parse, so a freshly resolved bundle picks a 3.x json and the app
465
+ # fails to encode any response with ArgumentError: unknown keyword:
466
+ # quirks_mode. Cap json at 2.x, which still accepts those keywords.
467
+ gemfile_lines << "gem 'json', '< 3'\n"
468
+
469
+ # Rails 6.0 and 6.1 need logger gem on Ruby 2.7+ due to stdlib changes
470
+ if rails_version && rails_version.to_f < 7.0 && RUBY_VERSION >= '2.7'
471
+ gemfile_lines << "gem 'logger'\n"
472
+ end
473
+
320
474
  if rails_version
321
475
  gemfile_lines.delete_if do |line|
322
476
  line =~ /gem ['"]rails['"]/
@@ -376,6 +530,17 @@ describe 'Mongoid application tests' do
376
530
  @clean_env ||= Hash[ENV.keys.grep(/BUNDLE|RUBYOPT/).map { |k| [k, nil ] }]
377
531
  end
378
532
 
533
+ def rails_env
534
+ # For Rails 6.0/6.1 on Ruby 2.7+, we need to require logger
535
+ # to fix "uninitialized constant ActiveSupport::LoggerThreadSafeLevel::Logger"
536
+ env = clean_env.dup
537
+ rails_version = SpecConfig.instance.rails_version
538
+ if rails_version && rails_version.to_f < 7.0 && RUBY_VERSION >= '2.7'
539
+ env['RUBYOPT'] = '-rlogger'
540
+ end
541
+ env
542
+ end
543
+
379
544
  def wait_for_port(port, timeout, process)
380
545
  deadline = Mongoid::Utils.monotonic_time + timeout
381
546
  loop do
@@ -87,8 +87,23 @@ describe 'has_and_belongs_to_many associations' do
87
87
  })
88
88
  end
89
89
 
90
- it 'does not raise on save' do
91
- expect { image_block.save! }.not_to raise_error
90
+ # The nested attributes are processed before the attachment_ids
91
+ # assignment is applied, so at that point the id is not yet in the
92
+ # association and resolving it requires a collection-wide lookup.
93
+ context 'when allow_reparenting_via_nested_attributes is false' do
94
+ config_override :allow_reparenting_via_nested_attributes, false
95
+
96
+ it 'raises a document not found error' do
97
+ expect { image_block.save! }.to raise_error(Mongoid::Errors::DocumentNotFound)
98
+ end
99
+ end
100
+
101
+ context 'when allow_reparenting_via_nested_attributes is true' do
102
+ config_override :allow_reparenting_via_nested_attributes, true
103
+
104
+ it 'does not raise on save' do
105
+ expect { image_block.save! }.not_to raise_error
106
+ end
92
107
  end
93
108
  end
94
109
 
@@ -268,10 +268,20 @@ describe "Dots and Dollars" do
268
268
  DADMUser.where("$_amount": 0).first
269
269
  end
270
270
 
271
- it "raise an error" do
271
+ it 'raises an error' do
272
272
  expect do
273
273
  queried
274
- end.to raise_error(Mongo::Error::OperationFailure)
274
+ end.to raise_error(Mongoid::Errors::InvalidQuery)
275
+ end
276
+
277
+ context 'when allow_unsafe_query_operators is true' do
278
+ config_override :allow_unsafe_query_operators, true
279
+
280
+ it 'raises an error from the server' do
281
+ expect do
282
+ queried
283
+ end.to raise_error(Mongo::Error::OperationFailure)
284
+ end
275
285
  end
276
286
  end
277
287
  end
@@ -34,19 +34,24 @@ describe 'Encryption' do
34
34
  around do |example|
35
35
  Mongoid.default_client[Crypt::Patient.collection_name].drop
36
36
  Mongoid.default_client[Crypt::Car.collection_name].drop
37
+ Mongoid.default_client[Crypt::Folder.collection_name].drop
37
38
  existing_key_id = Crypt::Patient.encrypt_metadata[:key_id]
38
39
  Crypt::Patient.set_key_id(data_key_id)
39
40
  Crypt::Car.set_key_id(data_key_id)
41
+ Crypt::Note.set_key_id(data_key_id)
40
42
  Mongoid::Config.send(:clients=, config)
41
43
  Mongoid::Clients.with_name(:key_vault)[key_vault_collection].drop
42
44
  Crypt::Patient.store_in(client: :encrypted)
43
45
  Crypt::Car.store_in(client: :encrypted, database: Crypt::Car.storage_options[:database])
46
+ Crypt::Folder.store_in(client: :encrypted)
44
47
 
45
48
  example.run
46
49
 
47
50
  Crypt::Patient.reset_storage_options!
51
+ Crypt::Folder.reset_storage_options!
48
52
  Crypt::Patient.set_key_id(existing_key_id)
49
53
  Crypt::Car.set_key_id(existing_key_id)
54
+ Crypt::Note.set_key_id(existing_key_id)
50
55
  end
51
56
 
52
57
  it 'encrypts and decrypts fields' do
@@ -86,6 +91,17 @@ describe 'Encryption' do
86
91
  end
87
92
  end
88
93
 
94
+ # Nothing about the parent document says it needs a schema: it has no
95
+ # encrypted field of its own and no encrypt_with. Its collection still needs
96
+ # one, or the embedded field goes out in the clear.
97
+ it 'stores an embedded field encrypted when the parent has no encrypted field' do
98
+ folder = Crypt::Folder.create!(note: Crypt::Note.new(text: 'SECRET'))
99
+ unencrypted_client[Crypt::Folder.collection.name].find(_id: folder.id).first.tap do |doc|
100
+ expect(doc['note']['text']).to be_a(BSON::Binary)
101
+ expect(doc['note']['text'].type).to eq(:ciphertext)
102
+ end
103
+ end
104
+
89
105
  it 'stores data encrypted in the non-default database' do
90
106
  car = Crypt::Car.create!(vin: 'VA1234')
91
107
  unencrypted_client
@@ -96,5 +112,138 @@ describe 'Encryption' do
96
112
  end
97
113
  end
98
114
 
115
+ # The encryption schema map is keyed by namespace and built once, when the
116
+ # client is constructed. Whenever the namespace an encrypted model actually
117
+ # writes to is not in that map, the driver encrypts nothing and reports no
118
+ # error, so the field lands in the clear. Mongoid fails closed instead.
119
+ describe 'when the target namespace is not in the encryption schema map' do
120
+ let(:scratch_databases) do
121
+ %w[vehicles_dynamic vehicles_tenant_b vehicles_tenant_c tenant_a]
122
+ end
123
+
124
+ around do |example|
125
+ original_options = {
126
+ Crypt::DynamicCar => Crypt::DynamicCar.storage_options,
127
+ Crypt::TenantCar => Crypt::TenantCar.storage_options
128
+ }
129
+ existing_key_ids = original_options.keys.to_h { |model| [ model, model.encrypt_metadata[:key_id] ] }
130
+ original_options.each_key do |model|
131
+ model.set_key_id(data_key_id)
132
+ model.store_in(client: :encrypted)
133
+ end
134
+ clean_scratch_data
135
+ # The schema map is generated when the client is built, so the client has
136
+ # to be built after the storage options above are in place.
137
+ Mongoid::Clients.clear
138
+
139
+ example.run
140
+
141
+ clean_scratch_data
142
+ original_options.each { |model, options| model.storage_options = options }
143
+ existing_key_ids.each { |model, key_id| model.set_key_id(key_id) }
144
+ Mongoid::Clients.clear
145
+ end
146
+
147
+ def clean_scratch_data
148
+ scratch_databases.each { |database| unencrypted_client.use(database).database.drop }
149
+ # Crypt::Car writes into the shared 'vehicles' database, which other
150
+ # examples rely on, so remove only the documents these examples create.
151
+ unencrypted_client
152
+ .use(Crypt::Car.storage_options[:database])[Crypt::Car.collection_name.to_s]
153
+ .delete_many(vin: { '$in' => %w[CLIENT-1 CLIENT-2] })
154
+ end
155
+
156
+ # Reads with a client that has no automatic encryption, to see what really
157
+ # landed on disk.
158
+ def documents_with_plaintext_vin(database, collection, vin)
159
+ unencrypted_client.use(database)[collection].find(vin: vin).to_a
160
+ end
161
+
162
+ context 'when the database name is a callable' do
163
+ it 'does not store the field in plaintext', :aggregate_failures do
164
+ expect { Crypt::DynamicCar.create!(vin: 'DYNAMIC-1') }
165
+ .to raise_error(Mongoid::Errors::NoEncryptionSchema)
166
+
167
+ expect(
168
+ documents_with_plaintext_vin('vehicles_dynamic', Crypt::DynamicCar.collection_name.to_s, 'DYNAMIC-1')
169
+ ).to be_empty
170
+ end
171
+ end
172
+
173
+ context 'when the database name is a callable resolved per tenant' do
174
+ around do |example|
175
+ Thread.current[:tenant_database] = 'tenant_a'
176
+ example.run
177
+ Thread.current[:tenant_database] = nil
178
+ end
179
+
180
+ it 'does not store the field in plaintext', :aggregate_failures do
181
+ expect { Crypt::TenantCar.create!(vin: 'TENANT-1') }
182
+ .to raise_error(Mongoid::Errors::NoEncryptionSchema)
183
+
184
+ expect(
185
+ documents_with_plaintext_vin('tenant_a', Crypt::TenantCar.collection_name.to_s, 'TENANT-1')
186
+ ).to be_empty
187
+ end
188
+ end
189
+
190
+ context 'when the database is overridden for the block' do
191
+ it 'does not store the field in plaintext', :aggregate_failures do
192
+ expect { Crypt::Car.with(database: 'vehicles_tenant_b') { |car| car.create!(vin: 'BLOCK-1') } }
193
+ .to raise_error(Mongoid::Errors::NoEncryptionSchema)
194
+
195
+ expect(
196
+ documents_with_plaintext_vin('vehicles_tenant_b', Crypt::Car.collection_name.to_s, 'BLOCK-1')
197
+ ).to be_empty
198
+ end
199
+ end
200
+
201
+ context 'when the database is overridden globally' do
202
+ persistence_context_override :database, 'vehicles_tenant_c'
99
203
 
204
+ it 'does not store the field in plaintext', :aggregate_failures do
205
+ expect { Crypt::Car.create!(vin: 'GLOBAL-1') }
206
+ .to raise_error(Mongoid::Errors::NoEncryptionSchema)
207
+
208
+ expect(
209
+ documents_with_plaintext_vin('vehicles_tenant_c', Crypt::Car.collection_name.to_s, 'GLOBAL-1')
210
+ ).to be_empty
211
+ end
212
+ end
213
+
214
+ context 'when the model is routed to a client without automatic encryption' do
215
+ it 'does not store the field in plaintext', :aggregate_failures do
216
+ expect { Crypt::Car.with(client: :default) { |car| car.create!(vin: 'CLIENT-1') } }
217
+ .to raise_error(Mongoid::Errors::NoEncryptionSchema)
218
+
219
+ expect(
220
+ documents_with_plaintext_vin('vehicles', Crypt::Car.collection_name.to_s, 'CLIENT-1')
221
+ ).to be_empty
222
+ end
223
+ end
224
+
225
+ context 'when the parent has no encrypted field of its own' do
226
+ it 'does not store the embedded field in plaintext', :aggregate_failures do
227
+ expect { Crypt::Folder.with(client: :default) { |folder| folder.create!(note: Crypt::Note.new(text: 'EMBEDDED-1')) } }
228
+ .to raise_error(Mongoid::Errors::NoEncryptionSchema)
229
+
230
+ expect(
231
+ unencrypted_client[Crypt::Folder.collection_name.to_s].find('note.text' => 'EMBEDDED-1').to_a
232
+ ).to be_empty
233
+ end
234
+ end
235
+
236
+ context 'when the client is overridden globally to one without automatic encryption' do
237
+ persistence_context_override :client, :default
238
+
239
+ it 'does not store the field in plaintext', :aggregate_failures do
240
+ expect { Crypt::Car.create!(vin: 'CLIENT-2') }
241
+ .to raise_error(Mongoid::Errors::NoEncryptionSchema)
242
+
243
+ expect(
244
+ documents_with_plaintext_vin('vehicles', Crypt::Car.collection_name.to_s, 'CLIENT-2')
245
+ ).to be_empty
246
+ end
247
+ end
248
+ end
100
249
  end
@@ -135,6 +135,27 @@
135
135
  $regex: bar
136
136
  matches: false
137
137
 
138
+ # Object#=~ is gone as of Ruby 3.2, so an element that cannot be matched
139
+ # against has to be skipped rather than passed to =~.
140
+ - name: array field value - element that cannot be matched against
141
+ document:
142
+ title:
143
+ - 42
144
+ - foo
145
+ query:
146
+ title:
147
+ $regex: foo
148
+ matches: true
149
+
150
+ - name: array field value - no element can be matched against
151
+ document:
152
+ title:
153
+ - 42
154
+ query:
155
+ title:
156
+ $regex: foo
157
+ matches: false
158
+
138
159
  - name: true field value
139
160
  document:
140
161
  title: true