mongoid 9.0.9 → 9.0.11

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5dca0df68ed7086b6c641b1021e556812e23872d5bd52a9777093e2d8cf1e64
4
- data.tar.gz: 24630d19104369a922b3a5a152012ae8eba3719576ef3c9e5f97304b6896a1c4
3
+ metadata.gz: b6976a4808058209fda8554306590eeb5fd44759d8c228d02bca544acbe5c200
4
+ data.tar.gz: e0dcd20719682c2a425d9ed17c1459376b6956311f9ec439c74843a4cefb8502
5
5
  SHA512:
6
- metadata.gz: 03013a0c28cda90cfc3b99c26d22e7672e8ce1c6449e599d7ae39a42d19ba742f37dea7510411ca004c7d5d5f25af80e9230bac63fef8ce43515711d7f669343
7
- data.tar.gz: 8bd5b0c4faa4f5f16fcc56ff612bae5a14fd40fa01513fda545469d225c6cd4909022c066719950147dc116dc1a7bdb79b69d48ad40454aabdb45481994dd69b
6
+ metadata.gz: 64ee6ee13a326ed9fae0e6b6dc08fa28ba0608a75342993b873023e4f87076160f6a8997e3edd53a273da4716c92d5aabc6b49f0931a489d2074d92f8de912d7
7
+ data.tar.gz: 491ecfb05581a08609bc41aedd39600f143a15f681405400c813402be95e11ed24d4be7363ac8079da9bc022d7d5b22ad3c53e534d41ee66a10977751a44b954
data/README.md CHANGED
@@ -21,9 +21,9 @@ Compatibility
21
21
 
22
22
  Mongoid supports and is tested against:
23
23
 
24
- - MRI 2.7 - 3.2
25
- - JRuby 9.4
26
- - MongoDB server 3.6 - 7.0
24
+ - MRI 2.7 - 4.0
25
+ - JRuby 9.4 and 10.0
26
+ - MongoDB server 3.6 - 8.2
27
27
 
28
28
  Issues
29
29
  ------
data/Rakefile CHANGED
@@ -68,6 +68,37 @@ RSpec::Core::RakeTask.new('spec:progress') do |spec|
68
68
  spec.pattern = "spec/**/*_spec.rb"
69
69
  end
70
70
 
71
+ RUBOCOPABLE = %w[ examples gemfiles perf lib spec mongoid.gemspec Gemfile Rakefile upload-api-docs ].freeze
72
+
73
+ desc 'Run rubocop'
74
+ task rubocop: %w[ rubocop:run ]
75
+
76
+ namespace :rubocop do
77
+ desc 'Run rubocop on the codebase'
78
+ task :run do
79
+ Bundler.with_unbundled_env do
80
+ sh 'bundle', 'exec', 'rubocop', *RUBOCOPABLE, verbose: false
81
+ end
82
+ end
83
+
84
+ desc 'Add a git pre-commit hook that runs rubocop'
85
+ task :install_hook do
86
+ hook_path = File.join('.git', 'hooks', 'pre-commit')
87
+ hook_script = <<~HOOK
88
+ #!/usr/bin/env bash
89
+ set -e
90
+
91
+ echo "Running rubocop..."
92
+ rake rubocop
93
+ HOOK
94
+
95
+ File.write(hook_path, hook_script)
96
+ FileUtils.chmod('+x', hook_path)
97
+
98
+ puts "Git pre-commit hook installed at #{hook_path}."
99
+ end
100
+ end
101
+
71
102
  desc 'Build and validate the evergreen config'
72
103
  task eg: %w[ eg:build eg:validate ]
73
104
 
@@ -184,11 +184,17 @@ module Mongoid
184
184
  else
185
185
  update_document(doc, attrs)
186
186
  end
187
- else
187
+ elsif association.embedded?
188
+ raise Errors::DocumentNotFound.new(association.klass, id)
189
+ elsif association.is_a?(Association::Referenced::HasAndBelongsToMany) || Mongoid.allow_reparenting_via_nested_attributes?
190
+ Mongoid::Warnings.warn_reparenting_via_nested_attributes if Mongoid.allow_reparenting_via_nested_attributes?
191
+
188
192
  # push existing document to association
189
193
  doc = association.klass.unscoped.find(converted)
190
194
  update_document(doc, attrs)
191
195
  existing.push(doc) unless destroyable?(attrs)
196
+ else
197
+ raise Errors::DocumentNotFound.new(association.klass, { _id: id, association.foreign_key => parent.id })
192
198
  end
193
199
 
194
200
  parent.children_may_have_changed!
@@ -32,13 +32,24 @@ module Mongoid
32
32
  Threaded.exit_autosave(self)
33
33
  end
34
34
 
35
- # Check if there is changes for auto-saving
35
+ # Check if there are changes for auto-saving. Returns true if the
36
+ # document is new, changed, or marked for destruction, or if any
37
+ # in-memory referenced child with autosave: true recursively
38
+ # satisfies the same condition.
36
39
  #
37
- # @example Return true if there is changes on self or in
38
- # autosaved associations.
39
- # document.changed_for_autosave?
40
- def changed_for_autosave?(doc)
41
- doc.new_record? || doc.changed? || doc.marked_for_destruction?
40
+ # The seen set prevents infinite recursion when autosave associations
41
+ # form a cycle (e.g. a belongs_to with autosave: true whose target
42
+ # has a has_many with autosave: true pointing back).
43
+ #
44
+ # @param [ Document ] doc The document to check.
45
+ # @param [ Set ] seen Documents already visited (cycle guard).
46
+ #
47
+ # @return [ true | false ] Whether the document needs autosaving.
48
+ def changed_for_autosave?(doc, seen = Set.new)
49
+ return false unless seen.add?(doc)
50
+
51
+ doc.new_record? || doc.changed? || doc.marked_for_destruction? ||
52
+ autosave_children_changed?(doc, seen)
42
53
  end
43
54
 
44
55
  # Define the autosave method on an association's owning class for
@@ -60,6 +71,8 @@ module Mongoid
60
71
  __autosaving__ do
61
72
  if assoc_value = ivar(association.name)
62
73
  Array(assoc_value).each do |doc|
74
+ next unless changed_for_autosave?(doc)
75
+
63
76
  pc = doc.persistence_context? ? doc.persistence_context : persistence_context.for_child(doc)
64
77
  doc.with(pc) do |d|
65
78
  d.save
@@ -72,6 +85,35 @@ module Mongoid
72
85
  klass.after_persist_parent save_method, unless: :autosaved?
73
86
  end
74
87
  end
88
+
89
+ private
90
+
91
+ # Returns true if any in-memory referenced child with autosave: true
92
+ # needs saving.
93
+ #
94
+ # @param [ Document ] doc The document whose children to check.
95
+ # @param [ Set ] seen Cycle guard passed through from changed_for_autosave?.
96
+ #
97
+ # @return [ true | false ]
98
+ def autosave_children_changed?(doc, seen)
99
+ if Mongoid.autosave_saves_unchanged_documents?
100
+ Mongoid::Warnings.warn_autosave_saves_unchanged_documents
101
+ return true
102
+ end
103
+
104
+ doc.class.relations.values.select { |a| a.autosave? && !a.embedded? }.any? do |assoc|
105
+ (assoc_value = doc.ivar(assoc.name)) &&
106
+ in_memory_docs(assoc_value).any? { |child| changed_for_autosave?(child, seen) }
107
+ end
108
+ end
109
+
110
+ # Returns the in-memory documents for an association value without
111
+ # triggering a database load of any unloaded documents. Association
112
+ # proxies expose in_memory for this purpose; a plain document (which
113
+ # belongs_to can store directly in the ivar) is itself in-memory.
114
+ def in_memory_docs(assoc_value)
115
+ assoc_value.respond_to?(:in_memory) ? assoc_value.in_memory : [ assoc_value ]
116
+ end
75
117
  end
76
118
  end
77
119
  end
@@ -104,7 +104,8 @@ module Mongoid
104
104
  # @api private
105
105
  def process_raw_attribute(name, raw, field)
106
106
  value = field ? field.demongoize(raw) : raw
107
- attribute_will_change!(name) if value.resizable?
107
+ is_relation = relations.key?(name)
108
+ attribute_will_change!(name) if value.resizable? && !is_relation
108
109
  value
109
110
  end
110
111
 
@@ -131,6 +131,10 @@ module Mongoid
131
131
  [MONGOID_WRAPPING_LIBRARY] + options[:wrapping_libraries]
132
132
  else
133
133
  [MONGOID_WRAPPING_LIBRARY]
134
+ end.tap do |wrap|
135
+ if defined?(::Rails) && ::Rails.respond_to?(:version)
136
+ wrap << { name: 'Rails', version: ::Rails.version }
137
+ end
134
138
  end
135
139
  options[:wrapping_libraries] = wrap_lib
136
140
  end
@@ -18,19 +18,23 @@ module Mongoid
18
18
  case version.to_s
19
19
  when /^[0-7]\./
20
20
  raise ArgumentError, "Version no longer supported: #{version}"
21
- when "8.0"
21
+
22
+ when '8.0'
22
23
  self.legacy_readonly = true
23
24
 
24
- load_defaults "8.1"
25
- when "8.1"
25
+ load_defaults '8.1'
26
+
27
+ when '8.1'
26
28
  self.immutable_ids = false
27
29
  self.legacy_persistence_context_behavior = true
28
30
  self.around_callbacks_for_embeds = true
29
31
  self.prevent_multiple_calls_of_embedded_callbacks = false
30
32
 
31
- load_defaults "9.0"
32
- when "9.0"
33
+ load_defaults '9.0'
34
+
35
+ when '9.0'
33
36
  # All flag defaults currently reflect 9.0 behavior.
37
+
34
38
  else
35
39
  raise ArgumentError, "Unknown version: #{version}"
36
40
  end
@@ -110,6 +110,31 @@ module Mongoid
110
110
  # to `:global_thread_pool`.
111
111
  option :global_executor_concurrency, default: nil
112
112
 
113
+ # When this flag is true, it will be possible to change the parent of a
114
+ # record in a "has_many" association by passing the child record's id in the
115
+ # nested attributes for another parent record.
116
+ #
117
+ # When this flag is false, attempting to change the parent of a record in a
118
+ # "has-many" association via nested attributes will raise an error.
119
+ #
120
+ # The default is `true`. Note that allowing reparenting via nested attributes
121
+ # is a potential security risk, since it could allow a malicious user to move
122
+ # records that they do not own to a parent record that they do own.
123
+ #
124
+ # This option will default to `false` in Mongoid 9.1, and will be removed
125
+ # in Mongoid 10.
126
+ option :allow_reparenting_via_nested_attributes, default: true
127
+
128
+ # When this flag is true, any documents in associations with `autosave: true`
129
+ # will be saved even if they have not been changed. When this flag is false,
130
+ # only autosaved documents that have been changed will be saved. The default
131
+ # is `true`.
132
+ #
133
+ # This option will default to `false` in Mongoid 9.1, and will be removed
134
+ # in Mongoid 10, with the only behavior at that point being as if this
135
+ # option were set to `false`.
136
+ option :autosave_saves_unchanged_documents, default: true
137
+
113
138
  # When this flag is false, a document will become read-only only once the
114
139
  # #readonly! method is called, and an error will be raised on attempting
115
140
  # to save or update such documents, instead of just on delete. When this
@@ -41,24 +41,57 @@ module Mongoid
41
41
  include Clients::Sessions
42
42
  include Options
43
43
 
44
+ # Allowed methods for from_hash to prevent arbitrary method execution.
45
+ # Only query-building methods are allowed, not execution or modification methods.
46
+ ALLOWED_FROM_HASH_METHODS = %i[
47
+ all all_in all_of and any_in any_of asc ascending
48
+ batch_size between
49
+ collation comment cursor_type
50
+ desc descending
51
+ elem_match eq exists extras
52
+ geo_spatial group gt gte
53
+ hint
54
+ in includes
55
+ limit lt lte
56
+ max_distance max_scan max_time_ms merge mod
57
+ ne near near_sphere nin no_timeout none none_of nor not not_in
58
+ offset only or order order_by
59
+ project
60
+ raw read reorder
61
+ scoped skip slice snapshot
62
+ text_search type
63
+ unscoped unwind
64
+ where with_size with_type without
65
+ ].freeze
66
+
44
67
  class << self
45
68
  # Convert the given hash to a criteria. Will iterate over each keys in the
46
- # hash which must correspond to method on a criteria object. The hash
47
- # must also include a "klass" key.
69
+ # hash which must correspond to an allowed method on a criteria object. The hash
70
+ # can include a "klass" key that specifies the model class for the criteria.
48
71
  #
49
72
  # @example Convert the hash to a criteria.
50
73
  # Criteria.from_hash({ klass: Band, where: { name: "Depeche Mode" })
51
74
  #
75
+ # @deprecated This method is deprecated and will
76
+ # be removed in a future release.
77
+ #
52
78
  # @param [ Hash ] hash The hash to convert.
53
79
  #
54
80
  # @return [ Criteria ] The criteria.
81
+ #
82
+ # @raise [ ArgumentError ] If a method is not allowed in from_hash.
55
83
  def from_hash(hash)
56
84
  criteria = Criteria.new(hash.delete(:klass) || hash.delete('klass'))
57
85
  hash.each_pair do |method, args|
58
- criteria = criteria.__send__(method, args)
86
+ method_sym = method.to_sym
87
+ unless ALLOWED_FROM_HASH_METHODS.include?(method_sym)
88
+ raise ArgumentError, "Method '#{method}' is not allowed in from_hash"
89
+ end
90
+ criteria = criteria.public_send(method_sym, args)
59
91
  end
60
92
  criteria
61
93
  end
94
+ Mongoid.deprecate(self, :from_hash)
62
95
  end
63
96
 
64
97
  # Static array used to check with method missing - we only need to ever
@@ -246,7 +279,8 @@ module Mongoid
246
279
  # criteria.merge(other_criteria)
247
280
  #
248
281
  # @example Merge the criteria with a hash. The hash must contain a klass
249
- # key and the key/value pairs correspond to method names/args.
282
+ # key that specifies the model class for the criteria and the key/value
283
+ # pairs correspond to method names/args.
250
284
  #
251
285
  # criteria.merge({
252
286
  # klass: Band,
@@ -254,7 +288,7 @@ module Mongoid
254
288
  # order_by: { name: 1 }
255
289
  # })
256
290
  #
257
- # @param [ Criteria ] other The other criterion to merge with.
291
+ # @param [ Criteria | Hash ] other The other criterion to merge with.
258
292
  #
259
293
  # @return [ Criteria ] A cloned self.
260
294
  def merge(other)
@@ -5,5 +5,5 @@ module Mongoid
5
5
  #
6
6
  # Note that this file is automatically updated via `rake candidate:create`.
7
7
  # Manual changes to this file will be overwritten by that rake task.
8
- VERSION = '9.0.9'
8
+ VERSION = '9.0.11'
9
9
  end
@@ -33,5 +33,9 @@ module Mongoid
33
33
  warning :symbol_type_deprecated, 'The BSON Symbol type is deprecated by MongoDB. Please use String or StringifiedSymbol field types instead of the Symbol field type.'
34
34
  warning :legacy_readonly, 'The readonly! method will only mark the document readonly when the legacy_readonly feature flag is switched off.'
35
35
  warning :mutable_ids, 'Ignoring updates to immutable attribute `_id`. Please set Mongoid::Config.immutable_ids to true and update your code so that `_id` is never updated.'
36
+ warning :reparenting_via_nested_attributes, 'Reparenting documents via nested attributes is insecure and is deprecated. Set Mongoid.allow_reparenting_via_nested_attributes to false and update your code to avoid reparenting documents via nested attributes.'
37
+ warning :autosave_saves_unchanged_documents, "Autosave associations are currently configured to save documents even if they haven't changed. " \
38
+ 'This legacy behavior is deprecated. Set Mongoid.autosave_saves_unchanged_documents to false to ' \
39
+ 'skip saving unchanged documents in autosave associations.'
36
40
  end
37
41
  end
@@ -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,12 @@ 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
+ # Rails 6.0 and 6.1 need logger gem on Ruby 2.7+ due to stdlib changes
463
+ if rails_version && rails_version.to_f < 7.0 && RUBY_VERSION >= '2.7'
464
+ gemfile_lines << "gem 'logger'\n"
465
+ end
466
+
320
467
  if rails_version
321
468
  gemfile_lines.delete_if do |line|
322
469
  line =~ /gem ['"]rails['"]/
@@ -376,6 +523,17 @@ describe 'Mongoid application tests' do
376
523
  @clean_env ||= Hash[ENV.keys.grep(/BUNDLE|RUBYOPT/).map { |k| [k, nil ] }]
377
524
  end
378
525
 
526
+ def rails_env
527
+ # For Rails 6.0/6.1 on Ruby 2.7+, we need to require logger
528
+ # to fix "uninitialized constant ActiveSupport::LoggerThreadSafeLevel::Logger"
529
+ env = clean_env.dup
530
+ rails_version = SpecConfig.instance.rails_version
531
+ if rails_version && rails_version.to_f < 7.0 && RUBY_VERSION >= '2.7'
532
+ env['RUBYOPT'] = '-rlogger'
533
+ end
534
+ env
535
+ end
536
+
379
537
  def wait_for_port(port, timeout, process)
380
538
  deadline = Mongoid::Utils.monotonic_time + timeout
381
539
  loop do
@@ -5,6 +5,53 @@ require "spec_helper"
5
5
  require_relative './referenced/has_many_models'
6
6
  require_relative './referenced/has_one_models'
7
7
 
8
+ # Models for the MONGOID-5751 regression test: after_save callbacks must not
9
+ # fire for pre-existing, unchanged documents when autosave: true cascades a
10
+ # save from a parent to its children.
11
+ module AutoSaveMONGOID5751
12
+ class Table
13
+ include Mongoid::Document
14
+
15
+ has_many :rows, autosave: true, class_name: 'AutoSaveMONGOID5751::Row',
16
+ inverse_of: :table
17
+
18
+ class << self
19
+ attr_accessor :after_save_count
20
+ end
21
+ self.after_save_count = 0
22
+
23
+ after_save { self.class.after_save_count += 1 }
24
+ end
25
+
26
+ class Row
27
+ include Mongoid::Document
28
+
29
+ belongs_to :table, class_name: 'AutoSaveMONGOID5751::Table', inverse_of: :rows
30
+ has_many :cells, autosave: true, class_name: 'AutoSaveMONGOID5751::Cell',
31
+ inverse_of: :row
32
+
33
+ class << self
34
+ attr_accessor :after_save_count
35
+ end
36
+ self.after_save_count = 0
37
+
38
+ after_save { self.class.after_save_count += 1 }
39
+ end
40
+
41
+ class Cell
42
+ include Mongoid::Document
43
+
44
+ belongs_to :row, class_name: 'AutoSaveMONGOID5751::Row', inverse_of: :cells
45
+
46
+ class << self
47
+ attr_accessor :after_save_count
48
+ end
49
+ self.after_save_count = 0
50
+
51
+ after_save { self.class.after_save_count += 1 }
52
+ end
53
+ end
54
+
8
55
  describe Mongoid::Association::Referenced::AutoSave do
9
56
 
10
57
  describe ".auto_save" do
@@ -399,6 +446,57 @@ describe Mongoid::Association::Referenced::AutoSave do
399
446
  expect(harvest.reload.season).to eq('Fall')
400
447
  end
401
448
  end
449
+
450
+ # Regression test for MONGOID-5751: after_save must not fire for
451
+ # pre-existing, unchanged documents that are merely loaded into memory
452
+ # as a side-effect of the autosave traversal.
453
+ context 'when a parent with existing children has a new child added' do
454
+ before do
455
+ # Persist a table with 3 pre-existing rows, each with 3 cells.
456
+ table = AutoSaveMONGOID5751::Table.create!
457
+ 3.times do
458
+ row = table.rows.create!
459
+ 3.times { row.cells.create! }
460
+ end
461
+
462
+ # Reset counters so only the saves triggered by the call below are
463
+ # measured.
464
+ AutoSaveMONGOID5751::Table.after_save_count = 0
465
+ AutoSaveMONGOID5751::Row.after_save_count = 0
466
+ AutoSaveMONGOID5751::Cell.after_save_count = 0
467
+
468
+ # Reload the table fresh from the database, then append exactly one
469
+ # new row (with one new cell) and persist.
470
+ reloaded = AutoSaveMONGOID5751::Table.find(table.id)
471
+ new_row = reloaded.rows.build
472
+ new_row.cells.build
473
+ reloaded.save!
474
+ end
475
+
476
+ it 'fires after_save once for the parent table' do
477
+ expect(AutoSaveMONGOID5751::Table.after_save_count).to eq(1)
478
+ end
479
+
480
+ it 'fires after_save only for the newly added cell' do
481
+ expect(AutoSaveMONGOID5751::Cell.after_save_count).to eq(1)
482
+ end
483
+
484
+ context 'when autosave_saves_unchanged_documents is true' do
485
+ config_override :autosave_saves_unchanged_documents, true
486
+
487
+ it 'fires after_save for all new and pre-existing rows' do
488
+ expect(AutoSaveMONGOID5751::Row.after_save_count).to eq(4)
489
+ end
490
+ end
491
+
492
+ context 'when autosave_saves_unchanged_documents is false' do
493
+ config_override :autosave_saves_unchanged_documents, false
494
+
495
+ it 'fires after_save only for the newly added row, not for pre-existing rows' do
496
+ expect(AutoSaveMONGOID5751::Row.after_save_count).to eq(1)
497
+ end
498
+ end
499
+ end
402
500
  end
403
501
  end
404
502
  end
@@ -164,24 +164,47 @@ describe Mongoid::Attributes::Nested do
164
164
  end
165
165
  end
166
166
 
167
- context "when the relation is a references many" do
168
-
167
+ context 'when the relation is a has-many' do
169
168
  before do
170
169
  Person.send(:undef_method, :posts_attributes=)
171
170
  Person.accepts_nested_attributes_for :posts
172
171
  end
173
172
 
174
- let(:person) do
175
- Person.new(posts_attributes: { "1" => { title: "First" }})
173
+ context 'when adding a new document to a relation' do
174
+ let(:person) do
175
+ Person.new(posts_attributes: { '1' => { title: 'First' } })
176
+ end
177
+
178
+ it 'sets the nested attributes' do
179
+ expect(person.posts.first.title).to eq('First')
180
+ end
176
181
  end
177
182
 
178
- it "sets the nested attributes" do
179
- expect(person.posts.first.title).to eq("First")
183
+ context 'when adding an existing document to a relation' do
184
+ let(:person1) { Person.create! }
185
+ let(:post) { person1.posts.create!(title: 'Sample Post') }
186
+
187
+ let(:person2) { Person.create!(posts_attributes: { '0' => { id: post.id, title: 'Reparented!' } }) }
188
+
189
+ context 'when allow_reparenting_via_nested_attributes is false' do
190
+ config_override :allow_reparenting_via_nested_attributes, false
191
+
192
+ it 'raises a document not found error' do
193
+ expect { person2 }.to raise_error(Mongoid::Errors::DocumentNotFound)
194
+ end
195
+ end
196
+
197
+ context 'when allow_reparenting_via_nested_attributes is true' do
198
+ config_override :allow_reparenting_via_nested_attributes, true
199
+
200
+ it 'sets the nested attributes' do
201
+ expect(person2.posts.map(&:title)).to eq([ 'Reparented!' ])
202
+ end
203
+ end
180
204
  end
181
205
  end
182
206
 
183
- context "when the relation is a references and referenced in many" do
184
-
207
+ context 'when the relation is a has-and-belongs-to-many' do
185
208
  before do
186
209
  Person.send(:undef_method, :preferences_attributes=)
187
210
  Person.accepts_nested_attributes_for :preferences
@@ -1355,10 +1378,11 @@ describe Mongoid::Attributes::Nested do
1355
1378
  context "when the ids do not match" do
1356
1379
 
1357
1380
  it "raises an error" do
1358
- expect {
1381
+ expect do
1359
1382
  person.addresses_attributes =
1360
- { "foo" => { "id" => "test", "street" => "Test" } }
1361
- }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Address with id\(s\)/)
1383
+ { 'foo' => { 'id' => 'test', 'street' => 'Test' } }
1384
+ end.to raise_error(Mongoid::Errors::DocumentNotFound,
1385
+ /Document\(s\) not found for class Address/)
1362
1386
  end
1363
1387
  end
1364
1388
  end
@@ -3012,12 +3036,12 @@ describe Mongoid::Attributes::Nested do
3012
3036
  end
3013
3037
 
3014
3038
  it "raises a document not found error" do
3015
- expect {
3039
+ expect do
3016
3040
  person.posts_attributes =
3017
- { "0" =>
3018
- { "id" => BSON::ObjectId.new.to_s, "title" => "Rogue" }
3019
- }
3020
- }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Post with id\(s\)/)
3041
+ { '0' =>
3042
+ { 'id' => BSON::ObjectId.new.to_s, 'title' => 'Rogue' } }
3043
+ end.to raise_error(Mongoid::Errors::DocumentNotFound,
3044
+ /Document\(s\) not found for class Post/)
3021
3045
  end
3022
3046
  end
3023
3047
  end
@@ -3048,10 +3072,11 @@ describe Mongoid::Attributes::Nested do
3048
3072
  context "when the ids do not match" do
3049
3073
 
3050
3074
  it "raises an error" do
3051
- expect {
3075
+ expect do
3052
3076
  person.posts_attributes =
3053
- { "foo" => { "id" => "test", "title" => "Test" } }
3054
- }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Post with id\(s\)/)
3077
+ { 'foo' => { 'id' => 'test', 'title' => 'Test' } }
3078
+ end.to raise_error(Mongoid::Errors::DocumentNotFound,
3079
+ /Document\(s\) not found for class Post/)
3055
3080
  end
3056
3081
  end
3057
3082
  end
@@ -3765,10 +3790,11 @@ describe Mongoid::Attributes::Nested do
3765
3790
  context "when the ids do not match" do
3766
3791
 
3767
3792
  it "raises an error" do
3768
- expect {
3793
+ expect do
3769
3794
  person.preferences_attributes =
3770
- { "foo" => { "id" => "test", "name" => "Test" } }
3771
- }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Preference with id\(s\)/)
3795
+ { 'foo' => { 'id' => 'test', 'name' => 'Test' } }
3796
+ end.to raise_error(Mongoid::Errors::DocumentNotFound,
3797
+ /Document\(s\) not found for class Preference/)
3772
3798
  end
3773
3799
  end
3774
3800
  end
@@ -2722,7 +2722,23 @@ describe Mongoid::Attributes do
2722
2722
  end
2723
2723
  end
2724
2724
 
2725
- context "when modifiying a set referenced with the [] notation" do
2725
+ context "when accessing an embedded document with the attribute accessor" do
2726
+ let(:band) { Band.create! }
2727
+
2728
+ before do
2729
+ Band.where(id: band.id).update_all({
2730
+ :$push => {records: { _id: BSON::ObjectId.new }}
2731
+ })
2732
+ end
2733
+
2734
+ it "does not throw a conflicting update error" do
2735
+ b1 = Band.find(band.id)
2736
+ b1[:records].is_a?(Array).should be true
2737
+ expect { b1.save! }.not_to raise_error
2738
+ end
2739
+ end
2740
+
2741
+ context "when modifying a set referenced with the [] notation" do
2726
2742
  let(:catalog) { Catalog.create!(set_field: [ 1 ].to_set) }
2727
2743
 
2728
2744
  before do
@@ -30,6 +30,34 @@ describe Mongoid::Clients::Factory do
30
30
  end
31
31
  end
32
32
 
33
+ shared_examples_for 'includes rails wrapping library' do
34
+ context 'when Rails is available' do
35
+ around do |example|
36
+ rails_was_defined = defined?(::Rails)
37
+
38
+ if !rails_was_defined
39
+ module ::Rails
40
+ def self.version
41
+ '6.1.0'
42
+ end
43
+ end
44
+ end
45
+
46
+ example.run
47
+
48
+ if !rails_was_defined
49
+ Object.send(:remove_const, :Rails) if defined?(::Rails)
50
+ end
51
+ end
52
+
53
+ it 'adds Rails as another wrapping library' do
54
+ expect(client.options[:wrapping_libraries]).to include(
55
+ {'name' => 'Rails', 'version' => '6.1.0'},
56
+ )
57
+ end
58
+ end
59
+ end
60
+
33
61
  describe ".create" do
34
62
 
35
63
  context "when provided a name" do
@@ -89,6 +117,8 @@ describe Mongoid::Clients::Factory do
89
117
  Mongoid::Clients::Factory::MONGOID_WRAPPING_LIBRARY)]
90
118
  end
91
119
 
120
+ it_behaves_like 'includes rails wrapping library'
121
+
92
122
  context 'when configuration specifies a wrapping library' do
93
123
 
94
124
  let(:config) do
@@ -110,6 +140,8 @@ describe Mongoid::Clients::Factory do
110
140
  {'name' => 'Foo'},
111
141
  ]
112
142
  end
143
+
144
+ it_behaves_like 'includes rails wrapping library'
113
145
  end
114
146
  end
115
147
 
@@ -3250,5 +3250,201 @@ describe Mongoid::Criteria do
3250
3250
  expect(criteria.selector).to eq({ 'name' => 'Songs Ohia' })
3251
3251
  end
3252
3252
  end
3253
+
3254
+ context 'with allowed methods' do
3255
+ context 'when using multiple query methods' do
3256
+ let(:hash) do
3257
+ {
3258
+ klass: Band,
3259
+ where: { active: true },
3260
+ limit: 10,
3261
+ skip: 5,
3262
+ order_by: { name: 1 }
3263
+ }
3264
+ end
3265
+
3266
+ it 'applies all methods successfully' do
3267
+ expect(criteria.selector).to eq({ 'active' => true })
3268
+ expect(criteria.options[:limit]).to eq(10)
3269
+ expect(criteria.options[:skip]).to eq(5)
3270
+ expect(criteria.options[:sort]).to eq({ 'name' => 1 })
3271
+ end
3272
+ end
3273
+
3274
+ context 'when using query selector methods' do
3275
+ let(:hash) do
3276
+ {
3277
+ klass: Band,
3278
+ gt: { members: 2 },
3279
+ in: { genre: ['rock', 'metal'] }
3280
+ }
3281
+ end
3282
+
3283
+ it 'applies selector methods' do
3284
+ expect(criteria.selector['members']).to eq({ '$gt' => 2 })
3285
+ expect(criteria.selector['genre']).to eq({ '$in' => ['rock', 'metal'] })
3286
+ end
3287
+ end
3288
+
3289
+ context 'when using aggregation methods' do
3290
+ let(:hash) do
3291
+ {
3292
+ klass: Band,
3293
+ project: { name: 1, members: 1 }
3294
+ }
3295
+ end
3296
+
3297
+ it 'applies aggregation methods' do
3298
+ expect { criteria }.not_to raise_error
3299
+ end
3300
+ end
3301
+ end
3302
+
3303
+ context 'with disallowed methods' do
3304
+ context 'when attempting to call create' do
3305
+ let(:hash) do
3306
+ { klass: Band, create: { name: 'Malicious' } }
3307
+ end
3308
+
3309
+ it 'raises ArgumentError' do
3310
+ expect { criteria }.to raise_error(ArgumentError, "Method 'create' is not allowed in from_hash")
3311
+ end
3312
+ end
3313
+
3314
+ context 'when attempting to call create!' do
3315
+ let(:hash) do
3316
+ { klass: Band, 'create!': { name: 'Malicious' } }
3317
+ end
3318
+
3319
+ it 'raises ArgumentError' do
3320
+ expect { criteria }.to raise_error(ArgumentError, "Method 'create!' is not allowed in from_hash")
3321
+ end
3322
+ end
3323
+
3324
+ context 'when attempting to call build' do
3325
+ let(:hash) do
3326
+ { klass: Band, build: { name: 'Malicious' } }
3327
+ end
3328
+
3329
+ it 'raises ArgumentError' do
3330
+ expect { criteria }.to raise_error(ArgumentError, "Method 'build' is not allowed in from_hash")
3331
+ end
3332
+ end
3333
+
3334
+ context 'when attempting to call find' do
3335
+ let(:hash) do
3336
+ { klass: Band, find: 'some_id' }
3337
+ end
3338
+
3339
+ it 'raises ArgumentError' do
3340
+ expect { criteria }.to raise_error(ArgumentError, "Method 'find' is not allowed in from_hash")
3341
+ end
3342
+ end
3343
+
3344
+ context 'when attempting to call execute_or_raise' do
3345
+ let(:hash) do
3346
+ { klass: Band, execute_or_raise: ['id1', 'id2'] }
3347
+ end
3348
+
3349
+ it 'raises ArgumentError' do
3350
+ expect { criteria }.to raise_error(ArgumentError, "Method 'execute_or_raise' is not allowed in from_hash")
3351
+ end
3352
+ end
3353
+
3354
+ context 'when attempting to call new' do
3355
+ let(:hash) do
3356
+ { klass: Band, new: { name: 'Test' } }
3357
+ end
3358
+
3359
+ it 'raises ArgumentError' do
3360
+ expect { criteria }.to raise_error(ArgumentError, "Method 'new' is not allowed in from_hash")
3361
+ end
3362
+ end
3363
+
3364
+ context 'when allowed method is combined with disallowed method' do
3365
+ let(:hash) do
3366
+ {
3367
+ klass: Band,
3368
+ where: { active: true },
3369
+ create: { name: 'Malicious' }
3370
+ }
3371
+ end
3372
+
3373
+ it 'raises ArgumentError before executing any methods' do
3374
+ expect { criteria }.to raise_error(ArgumentError, "Method 'create' is not allowed in from_hash")
3375
+ end
3376
+ end
3377
+ end
3378
+
3379
+ context 'security validation' do
3380
+ # This test ensures that ALL public methods not in the allowlist are blocked
3381
+ it 'blocks all dangerous public methods' do
3382
+ dangerous_methods = %i[
3383
+ build create create! new
3384
+ find find_or_create_by find_or_create_by! find_or_initialize_by
3385
+ first_or_create first_or_create! first_or_initialize
3386
+ execute_or_raise multiple_from_db for_ids
3387
+ documents= inclusions= scoping_options=
3388
+ initialize freeze as_json
3389
+ ]
3390
+
3391
+ dangerous_methods.each do |method|
3392
+ hash = { klass: Band, method => 'arg' }
3393
+ expect { described_class.from_hash(hash) }.to raise_error(
3394
+ ArgumentError,
3395
+ "Method '#{method}' is not allowed in from_hash"
3396
+ ), "Expected method '#{method}' to be blocked but it was allowed"
3397
+ end
3398
+ end
3399
+
3400
+ it 'blocks dangerous inherited methods from Object' do
3401
+ # Critical security test: block send, instance_eval, etc.
3402
+ inherited_dangerous = %i[
3403
+ send __send__ instance_eval instance_exec
3404
+ instance_variable_set method
3405
+ ]
3406
+
3407
+ inherited_dangerous.each do |method|
3408
+ hash = { klass: Band, method => 'arg' }
3409
+ expect { described_class.from_hash(hash) }.to raise_error(
3410
+ ArgumentError,
3411
+ "Method '#{method}' is not allowed in from_hash"
3412
+ ), "Expected inherited method '#{method}' to be blocked"
3413
+ end
3414
+ end
3415
+
3416
+ it 'blocks Enumerable execution methods' do
3417
+ # from_hash should build queries, not execute them
3418
+ enumerable_methods = %i[each map select count sum]
3419
+
3420
+ enumerable_methods.each do |method|
3421
+ hash = { klass: Band, method => 'arg' }
3422
+ expect { described_class.from_hash(hash) }.to raise_error(
3423
+ ArgumentError,
3424
+ "Method '#{method}' is not allowed in from_hash"
3425
+ ), "Expected Enumerable method '#{method}' to be blocked"
3426
+ end
3427
+ end
3428
+
3429
+ it 'allows all whitelisted methods' do
3430
+ # Sample of allowed methods from each category
3431
+ allowed_sample = {
3432
+ where: { name: 'Test' }, # Query selector
3433
+ limit: 10, # Query option
3434
+ skip: 5, # Query option
3435
+ gt: { age: 18 }, # Query selector
3436
+ in: { status: ['active'] }, # Query selector
3437
+ ascending: :name, # Sorting
3438
+ includes: :notes, # Eager loading
3439
+ merge: { klass: Band }, # Merge
3440
+ }
3441
+
3442
+ allowed_sample.each do |method, args|
3443
+ hash = { klass: Band, method => args }
3444
+ expect { described_class.from_hash(hash) }.not_to raise_error,
3445
+ "Expected method '#{method}' to be allowed but it was blocked"
3446
+ end
3447
+ end
3448
+ end
3253
3449
  end
3254
3450
  end
@@ -64,9 +64,16 @@ describe Mongoid::Errors::DocumentNotFound do
64
64
  end
65
65
 
66
66
  it "contains the problem in the message" do
67
- expect(error.message).to include(
68
- "Document not found for class Person with attributes {:name=>\"syd\"}."
69
- )
67
+ # Ruby 3.4+ changed Hash#inspect format from {:name=>"syd"} to {name: "syd"}
68
+ if RUBY_VERSION >= '3.4'
69
+ expect(error.message).to include(
70
+ "Document not found for class Person with attributes {name: \"syd\"}."
71
+ )
72
+ else
73
+ expect(error.message).to include(
74
+ "Document not found for class Person with attributes {:name=>\"syd\"}."
75
+ )
76
+ end
70
77
  end
71
78
 
72
79
  it "contains the summary in the message" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongoid
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.0.9
4
+ version: 9.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - The MongoDB Ruby Team
@@ -75,6 +75,20 @@ dependencies:
75
75
  - - "<"
76
76
  - !ruby/object:Gem::Version
77
77
  version: '2.0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: ostruct
80
+ requirement: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ type: :runtime
86
+ prerelease: false
87
+ version_requirements: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
78
92
  - !ruby/object:Gem::Dependency
79
93
  name: bson
80
94
  requirement: !ruby/object:Gem::Requirement
@@ -1231,7 +1245,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
1231
1245
  - !ruby/object:Gem::Version
1232
1246
  version: 1.3.6
1233
1247
  requirements: []
1234
- rubygems_version: 4.0.2
1248
+ rubygems_version: 4.0.10
1235
1249
  specification_version: 4
1236
1250
  summary: Elegant Persistence in Ruby for MongoDB.
1237
1251
  test_files: