mongoid 8.0.8 → 8.0.9

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +65 -41
  3. data/lib/mongoid/association/accessors.rb +5 -1
  4. data/lib/mongoid/association/eager_loadable.rb +3 -0
  5. data/lib/mongoid/config.rb +10 -0
  6. data/lib/mongoid/criteria/queryable/extensions/numeric.rb +15 -1
  7. data/lib/mongoid/document.rb +8 -1
  8. data/lib/mongoid/fields.rb +11 -6
  9. data/lib/mongoid/interceptable.rb +10 -8
  10. data/lib/mongoid/timestamps/created.rb +8 -1
  11. data/lib/mongoid/traversable.rb +12 -0
  12. data/lib/mongoid/validatable/associated.rb +6 -3
  13. data/lib/mongoid/version.rb +1 -1
  14. data/spec/integration/callbacks_models.rb +37 -0
  15. data/spec/integration/callbacks_spec.rb +27 -0
  16. data/spec/mongoid/association/eager_spec.rb +24 -2
  17. data/spec/mongoid/association/embedded/embeds_many_query_spec.rb +4 -0
  18. data/spec/mongoid/association_spec.rb +60 -0
  19. data/spec/mongoid/document_spec.rb +27 -0
  20. data/spec/mongoid/interceptable_spec.rb +100 -0
  21. data/spec/mongoid/interceptable_spec_models.rb +51 -111
  22. data/spec/mongoid/serializable_spec.rb +14 -14
  23. data/spec/mongoid/timestamps/created_spec.rb +23 -0
  24. data/spec/mongoid/validatable/associated_spec.rb +14 -4
  25. metadata +4 -80
  26. checksums.yaml.gz.sig +0 -0
  27. data/spec/shared/LICENSE +0 -20
  28. data/spec/shared/bin/get-mongodb-download-url +0 -17
  29. data/spec/shared/bin/s3-copy +0 -45
  30. data/spec/shared/bin/s3-upload +0 -69
  31. data/spec/shared/lib/mrss/child_process_helper.rb +0 -80
  32. data/spec/shared/lib/mrss/cluster_config.rb +0 -231
  33. data/spec/shared/lib/mrss/constraints.rb +0 -378
  34. data/spec/shared/lib/mrss/docker_runner.rb +0 -298
  35. data/spec/shared/lib/mrss/eg_config_utils.rb +0 -51
  36. data/spec/shared/lib/mrss/event_subscriber.rb +0 -210
  37. data/spec/shared/lib/mrss/lite_constraints.rb +0 -238
  38. data/spec/shared/lib/mrss/server_version_registry.rb +0 -113
  39. data/spec/shared/lib/mrss/session_registry.rb +0 -69
  40. data/spec/shared/lib/mrss/session_registry_legacy.rb +0 -60
  41. data/spec/shared/lib/mrss/spec_organizer.rb +0 -179
  42. data/spec/shared/lib/mrss/utils.rb +0 -37
  43. data/spec/shared/share/Dockerfile.erb +0 -321
  44. data/spec/shared/share/haproxy-1.conf +0 -16
  45. data/spec/shared/share/haproxy-2.conf +0 -17
  46. data/spec/shared/shlib/config.sh +0 -27
  47. data/spec/shared/shlib/distro.sh +0 -74
  48. data/spec/shared/shlib/server.sh +0 -416
  49. data/spec/shared/shlib/set_env.sh +0 -169
  50. data.tar.gz.sig +0 -0
  51. metadata.gz.sig +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d604924530b809dbef3eef08d7e3c52b14e815d674821789a98c590f704b149
4
- data.tar.gz: bfefc068fa68ed135560cff738dd7641f7df3780d01600a9f936752d5c760b8c
3
+ metadata.gz: 49662a14c4c7135e3403cab365f7e46928baf2efca9ede4e673269d39c5a7292
4
+ data.tar.gz: a69c22af01dbba7be588e2b4a9a63a1f58323ddd3395771969652323b2d7ecaf
5
5
  SHA512:
6
- metadata.gz: 50280304426a7dc55cb2e86d2ce69d295f5b51df6f714dee96e4a5c0b8da00c1ca09ad516d300da6755cf5160699a982868bb46c1afd2da812f952ca8c18e240
7
- data.tar.gz: 0fccee8bb9add55b0dfaa6454a33c89fe40553ce1a3fe9bef8be36fe4c92a884970d2e2b2a44c32761a198e750351276ef38f27a5e95f7fe11be02686425568b
6
+ metadata.gz: cdde64cee0c2f085b6f250aae4e0f31340a9b79d04177e69f9c287170d8bb2eaba435873d680f77181587624244c1ba5885eff2d3964f3ebbe5f0924a70b1347
7
+ data.tar.gz: 2d065d480c992dc3a2e772b7c33a0b50805d981c4f150f06b93f0ad9c89fd6b9c138c64354f8353c1a9849d9ddef45eae382bfc3ca1e4bb6aead94e60c5e6324
data/Rakefile CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler"
4
- require "bundler/gem_tasks"
5
4
  Bundler.setup
6
5
 
7
6
  ROOT = File.expand_path(File.join(File.dirname(__FILE__)))
@@ -10,34 +9,53 @@ $: << File.join(ROOT, 'spec/shared/lib')
10
9
 
11
10
  require "rake"
12
11
  require "rspec/core/rake_task"
13
- require 'mrss/spec_organizer'
14
- require 'rubygems/package'
15
- require 'rubygems/security/policies'
16
-
17
- def signed_gem?(path_to_gem)
18
- Gem::Package.new(path_to_gem, Gem::Security::HighSecurity).verify
19
- true
20
- rescue Gem::Security::Exception => e
21
- false
22
- end
23
-
24
- $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
25
- require "mongoid/version"
26
-
27
- tasks = Rake.application.instance_variable_get('@tasks')
28
- tasks['release:do'] = tasks.delete('release')
29
12
 
30
- task :gem => :build
13
+ # stands in for the Bundler-provided `build` task, which builds the
14
+ # gem for this project. Our release process builds the gems in a
15
+ # particular way, in a GitHub action. This task is just to help remind
16
+ # developers of that fact.
31
17
  task :build do
32
- system "gem build mongoid.gemspec"
18
+ abort <<~WARNING
19
+ `rake build` does nothing in this project. The gem must be built via
20
+ the `Mongoid Release` action on GitHub, which is triggered manually when
21
+ a new release is ready.
22
+ WARNING
33
23
  end
34
24
 
35
- task :install => :build do
36
- system "sudo gem install mongoid-#{Mongoid::VERSION}.gem"
25
+ # `rake version` is used by the deployment system so get the release version
26
+ # of the product beng deployed. It must do nothing more than just print the
27
+ # product version number.
28
+ #
29
+ # See the mongodb-labs/driver-github-tools/ruby/publish Github action.
30
+ desc "Print the current value of Mongoid::VERSION"
31
+ task :version do
32
+ require 'mongoid/version'
33
+
34
+ puts Mongoid::VERSION
37
35
  end
38
36
 
37
+ # overrides the default Bundler-provided `release` task, which also
38
+ # builds the gem. Our release process assumes the gem has already
39
+ # been built (and signed via GPG), so we just need `rake release` to
40
+ # push the gem to rubygems.
39
41
  task :release do
40
- raise "Please use ./release.sh to release"
42
+ require 'mongoid/version'
43
+
44
+ if ENV['GITHUB_ACTION'].nil?
45
+ abort <<~WARNING
46
+ `rake release` must be invoked from the `Mongoid Release` GitHub action,
47
+ and must not be invoked locally. This ensures the gem is properly signed
48
+ and distributed by the appropriate user.
49
+
50
+ Note that it is the `rubygems/release-gem@v1` step in the `Mongoid Release`
51
+ action that invokes this task. Do not rename or remove this task, or the
52
+ release-gem step will fail. Reimplement this task with caution.
53
+
54
+ mongoid-#{Mongoid::VERSION}.gem was NOT pushed to RubyGems.
55
+ WARNING
56
+ end
57
+
58
+ system 'gem', 'push', "mongoid-#{Mongoid::VERSION}.gem"
41
59
  end
42
60
 
43
61
  RSpec::Core::RakeTask.new("spec") do |spec|
@@ -64,6 +82,8 @@ RUN_PRIORITY = %i(
64
82
  )
65
83
 
66
84
  def spec_organizer
85
+ require 'mrss/spec_organizer'
86
+
67
87
  Mrss::SpecOrganizer.new(
68
88
  root: ROOT,
69
89
  classifiers: CLASSIFIERS,
@@ -99,32 +119,36 @@ task :docs => 'docs:yard'
99
119
  namespace :docs do
100
120
  desc "Generate yard documention"
101
121
  task :yard do
122
+ require "mongoid/version"
123
+
102
124
  out = File.join('yard-docs', Mongoid::VERSION)
103
125
  FileUtils.rm_rf(out)
104
126
  system "yardoc -o #{out} --title mongoid-#{Mongoid::VERSION}"
105
127
  end
106
128
  end
107
129
 
108
- namespace :release do
109
- task :check_private_key do
110
- unless File.exist?('gem-private_key.pem')
111
- raise "No private key present, cannot release"
112
- end
130
+ desc 'Build and validate the evergreen config'
131
+ task eg: %w[ eg:build eg:validate ]
132
+
133
+ # 'eg' == 'evergreen', but evergreen is too many letters for convenience
134
+ namespace :eg do
135
+ desc 'Builds the .evergreen/config.yml file from the templates'
136
+ task :build do
137
+ ruby '.evergreen/update-evergreen-configs'
113
138
  end
114
- end
115
139
 
116
- desc 'Verifies that all built gems in pkg/ are valid'
117
- task :verify do
118
- gems = Dir['pkg/*.gem']
119
- if gems.empty?
120
- puts 'There are no gems in pkg/ to verify'
121
- else
122
- gems.each do |gem|
123
- if signed_gem?(gem)
124
- puts "#{gem} is signed"
125
- else
126
- abort "#{gem} is not signed"
127
- end
128
- end
140
+ desc 'Validates the .evergreen/config.yml file'
141
+ task :validate do
142
+ system 'evergreen validate --project mongoid .evergreen/config.yml'
143
+ end
144
+
145
+ desc 'Updates the evergreen executable to the latest available version'
146
+ task :update do
147
+ system 'evergreen get-update --install'
148
+ end
149
+
150
+ desc 'Runs the current branch as an evergreen patch'
151
+ task :patch do
152
+ system 'evergreen patch --uncommitted --project mongoid --browse --auto-description --yes'
129
153
  end
130
154
  end
@@ -115,7 +115,11 @@ module Mongoid
115
115
  # during binding or when cascading callbacks. Whenever we retrieve
116
116
  # associations within the codebase, we use without_autobuild.
117
117
  if !without_autobuild? && association.embedded? && attribute_missing?(field_name)
118
- raise ActiveModel::MissingAttributeError, "Missing attribute: '#{field_name}'"
118
+ # We always allow accessing the parent document of an embedded one.
119
+ try_get_parent = association.is_a?(
120
+ Mongoid::Association::Embedded::EmbeddedIn
121
+ ) && field_name == association.key
122
+ raise ActiveModel::MissingAttributeError, "Missing attribute: '#{field_name}'" unless try_get_parent
119
123
  end
120
124
 
121
125
  if !reload && (value = ivar(name)) != false
@@ -31,6 +31,9 @@ module Mongoid
31
31
  docs_map = {}
32
32
  queue = [ klass.to_s ]
33
33
 
34
+ # account for single-collection inheritance
35
+ queue.push(klass.root_class.to_s) if klass != klass.root_class
36
+
34
37
  while klass = queue.shift
35
38
  if as = assoc_map.delete(klass)
36
39
  as.each do |assoc|
@@ -142,6 +142,16 @@ module Mongoid
142
142
  end
143
143
  end
144
144
 
145
+ # When this flag is true, callbacks for every embedded document will be
146
+ # called only once, even if the embedded document is embedded in multiple
147
+ # documents in the root document's dependencies graph.
148
+ # This will be the default in 9.0. Setting this flag to false restores the
149
+ # pre-9.0 behavior, where callbacks are called for every occurrence of an
150
+ # embedded document. The pre-9.0 behavior leads to a problem that for multi
151
+ # level nested documents callbacks are called multiple times.
152
+ # See https://jira.mongodb.org/browse/MONGOID-5542
153
+ option :prevent_multiple_calls_of_embedded_callbacks, default: false
154
+
145
155
  # When this flag is true, callbacks for embedded documents will not be
146
156
  # called. This is the default in 8.x, but will be changed to false in 9.0.
147
157
  #
@@ -43,7 +43,21 @@ module Mongoid
43
43
  #
44
44
  # @return [ Object ] The converted number.
45
45
  def __numeric__(object)
46
- object.to_s.match?(/\A[-+]?[0-9]*[0-9.]0*\z/) ? object.to_i : Float(object)
46
+ str = object.to_s
47
+ raise ArgumentError if str.empty?
48
+
49
+ # These requirements seem a bit odd, but they're explicitly specified in the tests,
50
+ # so we're obligated to keep them, for now. (This code was rewritten from a one-line
51
+ # regex, due to security concerns with a polynomial regex being used on uncontrolled
52
+ # data).
53
+
54
+ str = str.chop if str.end_with?('.')
55
+ return 0 if str.empty?
56
+
57
+ result = Integer(str) rescue Float(object)
58
+
59
+ integer = result.to_i
60
+ integer == result ? integer : result
47
61
  end
48
62
 
49
63
  # Evolve the object to an integer.
@@ -133,7 +133,14 @@ module Mongoid
133
133
  #
134
134
  # @return [ Hash ] A hash of all attributes in the hierarchy.
135
135
  def as_document
136
- BSON::Document.new(as_attributes)
136
+ attrs = as_attributes
137
+
138
+ # legacy attributes have a tendency to leak internal state via
139
+ # `as_document`; we have to deep_dup the attributes here to prevent
140
+ # that.
141
+ attrs = attrs.deep_dup if Mongoid.legacy_attributes
142
+
143
+ BSON::Document.new(attrs)
137
144
  end
138
145
 
139
146
  # Calls #as_json on the document with additional, Mongoid-specific options.
@@ -47,6 +47,11 @@ module Mongoid
47
47
  # @api private
48
48
  INVALID_BSON_CLASSES = [ BSON::Decimal128, BSON::Int32, BSON::Int64 ].freeze
49
49
 
50
+ # The suffix for generated translated fields.
51
+ #
52
+ # @api private
53
+ TRANSLATIONS_SFX = '_translations'
54
+
50
55
  module ClassMethods
51
56
  # Returns the list of id fields for this model class, as both strings
52
57
  # and symbols.
@@ -99,8 +104,8 @@ module Mongoid
99
104
  ar.each_with_index do |fn, i|
100
105
  key = fn
101
106
  unless klass.fields.key?(fn) || klass.relations.key?(fn)
102
- if tr = fn.match(/(.*)_translations\z/)&.captures&.first
103
- key = tr
107
+ if fn.end_with?(TRANSLATIONS_SFX)
108
+ key = fn.delete_suffix(TRANSLATIONS_SFX)
104
109
  else
105
110
  key = fn
106
111
  end
@@ -708,11 +713,11 @@ module Mongoid
708
713
  # @param [ String ] meth The name of the method.
709
714
  def create_translations_getter(name, meth)
710
715
  generated_methods.module_eval do
711
- re_define_method("#{meth}_translations") do
716
+ re_define_method("#{meth}#{TRANSLATIONS_SFX}") do
712
717
  attributes[name] ||= {}
713
718
  attributes[name].with_indifferent_access
714
719
  end
715
- alias_method :"#{meth}_t", :"#{meth}_translations"
720
+ alias_method :"#{meth}_t", :"#{meth}#{TRANSLATIONS_SFX}"
716
721
  end
717
722
  end
718
723
 
@@ -726,14 +731,14 @@ module Mongoid
726
731
  # @param [ Field ] field The field.
727
732
  def create_translations_setter(name, meth, field)
728
733
  generated_methods.module_eval do
729
- re_define_method("#{meth}_translations=") do |value|
734
+ re_define_method("#{meth}#{TRANSLATIONS_SFX}=") do |value|
730
735
  attribute_will_change!(name)
731
736
  value&.transform_values! do |_value|
732
737
  field.type.mongoize(_value)
733
738
  end
734
739
  attributes[name] = value
735
740
  end
736
- alias_method :"#{meth}_t=", :"#{meth}_translations="
741
+ alias_method :"#{meth}_t=", :"#{meth}#{TRANSLATIONS_SFX}="
737
742
  end
738
743
  end
739
744
 
@@ -141,9 +141,13 @@ module Mongoid
141
141
  # @api private
142
142
  def _mongoid_run_child_callbacks(kind, children: nil, &block)
143
143
  if Mongoid::Config.around_callbacks_for_embeds
144
- _mongoid_run_child_callbacks_with_around(kind, children: children, &block)
144
+ _mongoid_run_child_callbacks_with_around(kind,
145
+ children: children,
146
+ &block)
145
147
  else
146
- _mongoid_run_child_callbacks_without_around(kind, children: children, &block)
148
+ _mongoid_run_child_callbacks_without_around(kind,
149
+ children: children,
150
+ &block)
147
151
  end
148
152
  end
149
153
 
@@ -163,13 +167,14 @@ module Mongoid
163
167
  # @api private
164
168
  def _mongoid_run_child_callbacks_with_around(kind, children: nil, &block)
165
169
  child, *tail = (children || cascadable_children(kind))
170
+ with_children = !Mongoid::Config.prevent_multiple_calls_of_embedded_callbacks
166
171
  if child.nil?
167
172
  block&.call
168
173
  elsif tail.empty?
169
- child.run_callbacks(child_callback_type(kind, child), &block)
174
+ child.run_callbacks(child_callback_type(kind, child), with_children: with_children, &block)
170
175
  else
171
- child.run_callbacks(child_callback_type(kind, child)) do
172
- _mongoid_run_child_callbacks_with_around(kind, children: tail, &block)
176
+ child.run_callbacks(child_callback_type(kind, child), with_children: with_children) do
177
+ _mongoid_run_child_callbacks(kind, children: tail, &block)
173
178
  end
174
179
  end
175
180
  end
@@ -218,9 +223,6 @@ module Mongoid
218
223
  return false if env.halted
219
224
  env.value = !env.halted
220
225
  callback_list << [next_sequence, env]
221
- if (grandchildren = child.send(:cascadable_children, kind))
222
- _mongoid_run_child_before_callbacks(kind, children: grandchildren, callback_list: callback_list)
223
- end
224
226
  end
225
227
  callback_list
226
228
  end
@@ -22,13 +22,20 @@ module Mongoid
22
22
  # @example Set the created at time.
23
23
  # person.set_created_at
24
24
  def set_created_at
25
- if !timeless? && !created_at
25
+ if able_to_set_created_at?
26
26
  time = Time.configured.now
27
27
  self.updated_at = time if is_a?(Updated) && !updated_at_changed?
28
28
  self.created_at = time
29
29
  end
30
30
  clear_timeless_option
31
31
  end
32
+
33
+ # Is the created timestamp able to be set?
34
+ #
35
+ # @return [ true, false ] If the timestamp can be set.
36
+ def able_to_set_created_at?
37
+ !frozen? && !timeless? && !created_at
38
+ end
32
39
  end
33
40
  end
34
41
  end
@@ -300,6 +300,18 @@ module Mongoid
300
300
  !!(Mongoid::Document > superclass)
301
301
  end
302
302
 
303
+ # Returns the root class of the STI tree that the current
304
+ # class participates in. If the class is not an STI subclass, this
305
+ # returns the class itself.
306
+ #
307
+ # @return [ Mongoid::Document ] the root of the STI tree
308
+ def root_class
309
+ root = self
310
+ root = root.superclass while root.hereditary?
311
+
312
+ root
313
+ end
314
+
303
315
  # When inheriting, we want to copy the fields from the parent class and
304
316
  # set the on the child to start, mimicking the behavior of the old
305
317
  # class_inheritable_accessor that was deprecated in Rails edge.
@@ -69,13 +69,16 @@ module Mongoid
69
69
  # Now, treating the target as an array, look at each element
70
70
  # and see if it is valid, but only if it has already been
71
71
  # persisted, or changed, and hasn't been flagged for destroy.
72
- list.all? do |value|
73
- if value && !value.flagged_for_destroy? && (!value.persisted? || value.changed?)
72
+ #
73
+ # use map.all? instead of just all?, because all? will do short-circuit
74
+ # evaluation and terminate on the first failed validation.
75
+ list.map do |value|
76
+ if value && !value.flagged_for_destroy?
74
77
  value.validated? ? true : value.valid?
75
78
  else
76
79
  true
77
80
  end
78
- end
81
+ end.all?
79
82
  end
80
83
 
81
84
  document.errors.add(attribute, :invalid) unless valid
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mongoid
4
- VERSION = "8.0.8"
4
+ VERSION = "8.0.9"
5
5
  end
@@ -153,3 +153,40 @@ class Building
153
153
 
154
154
  has_and_belongs_to_many :architects, dependent: :nullify
155
155
  end
156
+
157
+ class Root
158
+ include Mongoid::Document
159
+ embeds_many :embedded_once, cascade_callbacks: true
160
+ after_save :trace
161
+
162
+ attr_accessor :logger
163
+
164
+ def trace
165
+ logger << :root
166
+ end
167
+ end
168
+
169
+ class EmbeddedOnce
170
+ include Mongoid::Document
171
+ embeds_many :embedded_twice, cascade_callbacks: true
172
+ embedded_in :root
173
+ after_save :trace
174
+
175
+ attr_accessor :logger
176
+
177
+ def trace
178
+ logger << :embedded_once
179
+ end
180
+ end
181
+
182
+ class EmbeddedTwice
183
+ include Mongoid::Document
184
+ embedded_in :embedded_once
185
+ after_save :trace
186
+
187
+ attr_accessor :logger
188
+
189
+ def trace
190
+ logger << :embedded_twice
191
+ end
192
+ end
@@ -467,4 +467,31 @@ describe 'callbacks integration tests' do
467
467
  expect { book.save! }.not_to raise_error(SystemStackError)
468
468
  end
469
469
  end
470
+
471
+ context 'nested embedded documents' do
472
+ config_override :prevent_multiple_calls_of_embedded_callbacks, true
473
+
474
+ let(:logger) { Array.new }
475
+
476
+ let(:root) do
477
+ Root.new(
478
+ embedded_once: [
479
+ EmbeddedOnce.new(
480
+ embedded_twice: [EmbeddedTwice.new]
481
+ )
482
+ ]
483
+ )
484
+ end
485
+
486
+ before(:each) do
487
+ root.logger = logger
488
+ root.embedded_once.first.logger = logger
489
+ root.embedded_once.first.embedded_twice.first.logger = logger
490
+ end
491
+
492
+ it 'runs callbacks in the correct order' do
493
+ root.save!
494
+ expect(logger).to eq(%i[embedded_twice embedded_once root])
495
+ end
496
+ end
470
497
  end
@@ -14,14 +14,36 @@ describe Mongoid::Association::EagerLoadable do
14
14
  Mongoid::Contextual::Mongo.new(criteria)
15
15
  end
16
16
 
17
+ let(:association_host) { Account }
18
+
17
19
  let(:inclusions) do
18
20
  includes.map do |key|
19
- Account.reflect_on_association(key)
21
+ association_host.reflect_on_association(key)
20
22
  end
21
23
  end
22
24
 
23
25
  let(:doc) { criteria.first }
24
26
 
27
+ context 'when root is an STI subclass' do
28
+ # Driver has_one Vehicle
29
+ # Vehicle belongs_to Driver
30
+ # Truck is a Vehicle
31
+
32
+ before do
33
+ Driver.create!(vehicle: Truck.new)
34
+ end
35
+
36
+ let(:criteria) { Truck.all }
37
+ let(:includes) { %i[ driver ] }
38
+ let(:association_host) { Truck }
39
+
40
+ it 'preloads the driver' do
41
+ expect(doc.ivar(:driver)).to be false
42
+ context.preload(inclusions, [ doc ])
43
+ expect(doc.ivar(:driver)).to be == Driver.first
44
+ end
45
+ end
46
+
25
47
  context "when belongs_to" do
26
48
 
27
49
  let!(:account) do
@@ -42,7 +64,7 @@ describe Mongoid::Association::EagerLoadable do
42
64
  it "preloads the parent" do
43
65
  expect(doc.ivar(:person)).to be false
44
66
  context.preload(inclusions, [doc])
45
- expect(doc.ivar(:person)).to eq(doc.person)
67
+ expect(doc.ivar(:person)).to be == person
46
68
  end
47
69
  end
48
70
 
@@ -27,6 +27,10 @@ describe Mongoid::Association::Embedded::EmbedsMany do
27
27
  expect(legislator.attributes.keys).to eq(['_id', 'a'])
28
28
  end
29
29
 
30
+ it 'allows accessing the parent' do
31
+ expect { legislator.congress }.not_to raise_error
32
+ end
33
+
30
34
  context 'when using only with $' do
31
35
  before do
32
36
  Patient.destroy_all
@@ -100,6 +100,66 @@ describe Mongoid::Association do
100
100
  expect(name).to_not be_an_embedded_many
101
101
  end
102
102
  end
103
+
104
+ context "when validation depends on association" do
105
+ before(:all) do
106
+ class Author
107
+ include Mongoid::Document
108
+ embeds_many :books, cascade_callbacks: true
109
+ field :condition, type: Boolean
110
+ end
111
+
112
+ class Book
113
+ include Mongoid::Document
114
+ embedded_in :author
115
+ validate :parent_condition_is_not_true
116
+
117
+ def parent_condition_is_not_true
118
+ return unless author&.condition
119
+ errors.add :base, "Author condition is true."
120
+ end
121
+ end
122
+
123
+ Author.delete_all
124
+ Book.delete_all
125
+ end
126
+
127
+ let(:author) { Author.new }
128
+ let(:book) { Book.new }
129
+
130
+ context "when author is not persisted" do
131
+ it "is valid without books" do
132
+ expect(author.valid?).to be true
133
+ end
134
+
135
+ it "is valid with a book" do
136
+ author.books << book
137
+ expect(author.valid?).to be true
138
+ end
139
+
140
+ it "is not valid when condition is true with a book" do
141
+ author.condition = true
142
+ author.books << book
143
+ expect(author.valid?).to be false
144
+ end
145
+ end
146
+
147
+ context "when author is persisted" do
148
+ before do
149
+ author.books << book
150
+ author.save
151
+ end
152
+
153
+ it "remains valid initially" do
154
+ expect(author.valid?).to be true
155
+ end
156
+
157
+ it "becomes invalid when condition is set to true" do
158
+ author.update_attributes(condition: true)
159
+ expect(author.valid?).to be false
160
+ end
161
+ end
162
+ end
103
163
  end
104
164
 
105
165
  describe "#embedded_one?" do
@@ -591,6 +591,33 @@ describe Mongoid::Document do
591
591
  expect(person.as_document["addresses"].first).to have_key(:locations)
592
592
  end
593
593
 
594
+ context 'when modifying the returned object' do
595
+ let(:record) do
596
+ RootCategory.create(categories: [{ name: 'tests' }]).reload
597
+ end
598
+
599
+ shared_examples_for 'an object with protected internal state' do
600
+ it 'does not expose internal state' do
601
+ before_change = record.as_document.dup
602
+ record.categories.first.name = 'things'
603
+ after_change = record.as_document
604
+ expect(before_change['categories'].first['name']).not_to eq('things')
605
+ end
606
+ end
607
+
608
+ context 'when legacy_attributes is true' do
609
+ config_override :legacy_attributes, true
610
+
611
+ it_behaves_like 'an object with protected internal state'
612
+ end
613
+
614
+ context 'when legacy_attributes is false' do
615
+ config_override :legacy_attributes, false
616
+
617
+ it_behaves_like 'an object with protected internal state'
618
+ end
619
+ end
620
+
594
621
  context "with relation define store_as option in embeded_many" do
595
622
 
596
623
  let!(:phone) do