mongoid 8.0.8 → 8.0.9

Sign up to get free protection for your applications and to get access to all the features.
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