mongoid 9.1.0 → 9.1.1
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 +4 -4
- data/lib/config/locales/en.yml +49 -0
- data/lib/mongoid/association/accessors.rb +9 -1
- data/lib/mongoid/association/depending.rb +14 -10
- data/lib/mongoid/association/eager.rb +9 -0
- data/lib/mongoid/association/eager_load/discriminated_inclusion.rb +88 -0
- data/lib/mongoid/association/eager_load/embedded_distributor.rb +151 -0
- data/lib/mongoid/association/eager_load/inclusion.rb +173 -0
- data/lib/mongoid/association/eager_load/inclusion_tree.rb +96 -0
- data/lib/mongoid/association/eager_load/lookup_pipeline.rb +121 -0
- data/lib/mongoid/association/eager_load/polymorphic_preloader.rb +61 -0
- data/lib/mongoid/association/eager_load/polymorphic_targets.rb +151 -0
- data/lib/mongoid/association/eager_loadable.rb +57 -100
- data/lib/mongoid/association/embedded/embeds_many/proxy.rb +6 -1
- data/lib/mongoid/association/embedded/embeds_one.rb +1 -0
- data/lib/mongoid/association/macros.rb +13 -1
- data/lib/mongoid/association/nested/many.rb +34 -5
- data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
- data/lib/mongoid/association/nested/one.rb +6 -1
- data/lib/mongoid/association/options.rb +17 -0
- data/lib/mongoid/association/referenced/belongs_to/eager.rb +6 -1
- data/lib/mongoid/association/referenced/belongs_to.rb +1 -0
- data/lib/mongoid/association/referenced/counter_cache.rb +3 -3
- data/lib/mongoid/association/referenced/has_and_belongs_to_many.rb +5 -0
- data/lib/mongoid/association/referenced/has_many/enumerable.rb +3 -1
- data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
- data/lib/mongoid/association/referenced/has_many_through/eager.rb +99 -0
- data/lib/mongoid/association/referenced/has_many_through/proxy.rb +68 -0
- data/lib/mongoid/association/referenced/has_many_through.rb +201 -0
- data/lib/mongoid/association/referenced/has_one.rb +1 -0
- data/lib/mongoid/association/referenced/has_one_through/eager.rb +65 -0
- data/lib/mongoid/association/referenced/has_one_through/proxy.rb +30 -0
- data/lib/mongoid/association/referenced/has_one_through.rb +167 -0
- data/lib/mongoid/association/referenced.rb +2 -0
- data/lib/mongoid/association/relatable.rb +40 -0
- data/lib/mongoid/association.rb +7 -0
- data/lib/mongoid/clients/factory.rb +7 -13
- data/lib/mongoid/collection_configurable.rb +8 -0
- data/lib/mongoid/config/defaults.rb +0 -1
- data/lib/mongoid/config/encryption.rb +36 -19
- data/lib/mongoid/config.rb +56 -5
- data/lib/mongoid/contextual/aggregable/memory.rb +6 -2
- data/lib/mongoid/contextual/memory.rb +18 -7
- data/lib/mongoid/criteria/includable.rb +35 -11
- data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
- data/lib/mongoid/criteria/queryable/selectable.rb +123 -7
- data/lib/mongoid/deprecable.rb +45 -2
- data/lib/mongoid/encryptable.rb +46 -0
- data/lib/mongoid/errors/config_redactor.rb +43 -0
- data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
- data/lib/mongoid/errors/mixed_client_configuration.rb +1 -1
- data/lib/mongoid/errors/no_client_database.rb +1 -1
- data/lib/mongoid/errors/no_client_hosts.rb +1 -1
- data/lib/mongoid/errors/no_encryption_schema.rb +28 -0
- data/lib/mongoid/errors/readonly_association.rb +29 -0
- data/lib/mongoid/errors.rb +4 -0
- data/lib/mongoid/extensions/vector.rb +64 -0
- data/lib/mongoid/extensions.rb +1 -0
- data/lib/mongoid/field_readable.rb +69 -0
- data/lib/mongoid/indexable.rb +38 -26
- data/lib/mongoid/matchable.rb +6 -1
- data/lib/mongoid/matcher/eq_impl_with_regexp.rb +2 -4
- data/lib/mongoid/matcher/regex.rb +11 -12
- data/lib/mongoid/matcher/regexp_budget.rb +372 -0
- data/lib/mongoid/matcher.rb +1 -0
- data/lib/mongoid/persistable/creatable.rb +1 -1
- data/lib/mongoid/persistable/updatable.rb +9 -11
- data/lib/mongoid/persistence_context.rb +35 -0
- data/lib/mongoid/search_indexable.rb +64 -23
- data/lib/mongoid/serializable.rb +12 -4
- data/lib/mongoid/tasks/database.rb +23 -9
- data/lib/mongoid/threaded.rb +36 -0
- data/lib/mongoid/timestamps/timeless.rb +84 -10
- data/lib/mongoid/traversable.rb +2 -2
- data/lib/mongoid/validatable/associated.rb +18 -2
- data/lib/mongoid/version.rb +1 -1
- data/spec/integration/app_spec.rb +7 -1
- data/spec/integration/associations/belongs_to_spec.rb +13 -0
- data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
- data/spec/integration/dots_and_dollars_spec.rb +12 -2
- data/spec/integration/encryption_spec.rb +151 -0
- data/spec/integration/matcher_operator_data/regex.yml +21 -0
- data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
- data/spec/integration/query_operator_guard_spec.rb +89 -0
- data/spec/mongoid/association/eager_loadable_spec.rb +47 -0
- data/spec/mongoid/association/eager_spec.rb +2 -2
- data/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +33 -0
- data/spec/mongoid/association/fallback_spec.rb +425 -0
- data/spec/mongoid/association/macros_spec.rb +26 -0
- data/spec/mongoid/association/referenced/belongs_to/eager_spec.rb +17 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many_spec.rb +50 -0
- data/spec/mongoid/association/referenced/has_many/eager_spec.rb +33 -0
- data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
- data/spec/mongoid/association/referenced/has_many_through/eager_spec.rb +58 -0
- data/spec/mongoid/association/referenced/has_many_through/proxy_spec.rb +62 -0
- data/spec/mongoid/association/referenced/has_many_through_spec.rb +399 -0
- data/spec/mongoid/association/referenced/has_one_through/eager_spec.rb +58 -0
- data/spec/mongoid/association/referenced/has_one_through/proxy_spec.rb +49 -0
- data/spec/mongoid/association/referenced/has_one_through_spec.rb +211 -0
- data/spec/mongoid/attributes/nested_spec.rb +249 -8
- data/spec/mongoid/config/defaults_spec.rb +33 -0
- data/spec/mongoid/config/encryption_spec.rb +228 -0
- data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
- data/spec/mongoid/contextual/memory_spec.rb +177 -0
- data/spec/mongoid/criteria/includable_spec.rb +875 -0
- data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +15 -1
- data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +201 -0
- data/spec/mongoid/criteria_spec.rb +3 -0
- data/spec/mongoid/encryptable_spec.rb +61 -0
- data/spec/mongoid/errors/config_redactor_spec.rb +112 -0
- data/spec/mongoid/errors/mixed_client_configuration_spec.rb +31 -0
- data/spec/mongoid/errors/no_client_database_spec.rb +29 -0
- data/spec/mongoid/errors/no_client_hosts_spec.rb +26 -0
- data/spec/mongoid/errors/readonly_association_spec.rb +34 -0
- data/spec/mongoid/extensions/vector_spec.rb +175 -0
- data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
- data/spec/mongoid/search_indexable_spec.rb +295 -9
- data/spec/mongoid/tasks/database_spec.rb +18 -0
- data/spec/mongoid/timestamps/timeless_spec.rb +197 -5
- data/spec/mongoid/touchable_spec.rb +25 -0
- data/spec/mongoid/touchable_spec_models.rb +10 -0
- data/spec/mongoid/validatable/associated_spec.rb +19 -0
- data/spec/spec_helper.rb +5 -1
- data/spec/support/crypt/models.rb +157 -0
- metadata +52 -2
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'benchmark'
|
|
4
|
+
require 'spec_helper'
|
|
5
|
+
|
|
6
|
+
describe 'in-memory regexp time limit' do
|
|
7
|
+
# See the comment in spec/mongoid/matcher/regexp_budget_spec.rb: costly to
|
|
8
|
+
# match but bounded on every supported Ruby. A pattern relying on nested
|
|
9
|
+
# quantifiers would hang before Ruby 3.2, where the examples that have no
|
|
10
|
+
# interrupt available could not stop it.
|
|
11
|
+
let(:slow_pattern) { "(?:#{(1..300).map { |i| "a#{i}" }.join('|')})Z" }
|
|
12
|
+
let(:slow_street) { 'a' * 5_000 }
|
|
13
|
+
|
|
14
|
+
let(:person) do
|
|
15
|
+
# Address derives its _id from the street, so the streets have to differ or
|
|
16
|
+
# the embedded association collapses to a single document.
|
|
17
|
+
Person.new(addresses: Array.new(30) { |i| Address.new(street: "#{slow_street}#{i}") })
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
let(:one_address) { Person.new(addresses: [ Address.new(street: slow_street) ]) }
|
|
21
|
+
|
|
22
|
+
# What one match of the fixture costs on the machine running the suite. It
|
|
23
|
+
# varies by more than an order of magnitude between implementations - around
|
|
24
|
+
# 6ms on MRI, 110ms on JRuby - so the examples below calibrate against a
|
|
25
|
+
# measurement instead of hard-coding a threshold that only holds on one.
|
|
26
|
+
let(:single_match_cost) do
|
|
27
|
+
regexp = Regexp.new(slow_pattern)
|
|
28
|
+
3.times { slow_street =~ regexp }
|
|
29
|
+
Benchmark.realtime { slow_street =~ regexp }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Comfortably more than one match, comfortably less than twenty. The examples
|
|
33
|
+
# only need the ordering to hold, so the margin either side is wide.
|
|
34
|
+
let(:calibrated_limit) { single_match_cost * 5 }
|
|
35
|
+
|
|
36
|
+
context 'when the limit is disabled' do
|
|
37
|
+
config_override :in_memory_regexp_time_limit, nil
|
|
38
|
+
|
|
39
|
+
it 'evaluates the query without a bound' do
|
|
40
|
+
expect(person.addresses.where(street: { '$regex' => slow_pattern }).to_a).to eq([])
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
context 'when a limit is configured' do
|
|
45
|
+
config_override :in_memory_regexp_time_limit, nil
|
|
46
|
+
|
|
47
|
+
before { Mongoid::Config.in_memory_regexp_time_limit = calibrated_limit }
|
|
48
|
+
|
|
49
|
+
it 'leaves ordinary queries alone' do
|
|
50
|
+
person = Person.new(addresses: [ Address.new(street: 'Clarkson') ])
|
|
51
|
+
expect(person.addresses.where(street: { '$regex' => '\AClark' }).to_a.size).to eq(1)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'does not raise for a single document' do
|
|
55
|
+
# The counterpart to the example below, and what makes it meaningful: one
|
|
56
|
+
# document stays under the limit, so a scan that raises can only have got
|
|
57
|
+
# there by accumulating cost across documents.
|
|
58
|
+
expect do
|
|
59
|
+
one_address.addresses.where(street: { '$regex' => slow_pattern }).to_a
|
|
60
|
+
end.not_to raise_error
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'raises for a scan over many documents under that same limit' do
|
|
64
|
+
expect do
|
|
65
|
+
person.addresses.where(street: { '$regex' => slow_pattern }).to_a
|
|
66
|
+
end.to raise_error(Mongoid::Errors::InMemoryRegexpTimeout)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'raises when $in multiplies the cost within a single document' do
|
|
70
|
+
conditions = Array.new(20) { BSON::Regexp::Raw.new(slow_pattern) }
|
|
71
|
+
|
|
72
|
+
expect do
|
|
73
|
+
one_address.addresses.where(street: { '$in' => conditions }).to_a
|
|
74
|
+
end.to raise_error(Mongoid::Errors::InMemoryRegexpTimeout)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'raises through the public _matches? API' do
|
|
78
|
+
# 20 conditions rather than one, because a single match is deliberately
|
|
79
|
+
# kept well under the limit by the calibration above.
|
|
80
|
+
conditions = Array.new(20) { BSON::Regexp::Raw.new(slow_pattern) }
|
|
81
|
+
|
|
82
|
+
expect do
|
|
83
|
+
person.addresses.first._matches?('street' => { '$in' => conditions })
|
|
84
|
+
end.to raise_error(Mongoid::Errors::InMemoryRegexpTimeout)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
context 'when the pattern is pathological' do
|
|
89
|
+
config_override :in_memory_regexp_time_limit, 0.2
|
|
90
|
+
|
|
91
|
+
# The ticket's Case 1, verbatim. It backtracks exponentially and is the
|
|
92
|
+
# reason the guard exists, so every entry point that evaluates a selector
|
|
93
|
+
# in memory has to bound it. The subject is kept short enough that the
|
|
94
|
+
# match still finishes on its own in a few seconds, so an entry point that
|
|
95
|
+
# fails to bound it shows up as a failing example rather than a hung run.
|
|
96
|
+
let(:evil_pattern) { '^(a+)+\1?$' }
|
|
97
|
+
let(:evil_subject) { "#{'a' * 28}X" }
|
|
98
|
+
|
|
99
|
+
it 'bounds a scan over an embedded association' do
|
|
100
|
+
person = Person.new(addresses: [ Address.new(street: evil_subject) ])
|
|
101
|
+
|
|
102
|
+
expect do
|
|
103
|
+
person.addresses.where(street: { '$regex' => evil_pattern }).to_a
|
|
104
|
+
end.to raise_error(Mongoid::Errors::InMemoryRegexpTimeout)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'bounds the public _matches? API' do
|
|
108
|
+
expect do
|
|
109
|
+
Address.new(street: evil_subject)._matches?('street' => { '$regex' => evil_pattern })
|
|
110
|
+
end.to raise_error(Mongoid::Errors::InMemoryRegexpTimeout)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'bounds removal from a referenced association' do
|
|
114
|
+
owner = Person.create!
|
|
115
|
+
owner.posts.create!(title: evil_subject)
|
|
116
|
+
|
|
117
|
+
expect do
|
|
118
|
+
owner.posts.delete_all(title: { '$regex' => evil_pattern })
|
|
119
|
+
end.to raise_error(Mongoid::Errors::InMemoryRegexpTimeout)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'deletes nothing when removal is bounded' do
|
|
123
|
+
# The scan runs before the delete, so the error means what a caller would
|
|
124
|
+
# take it to mean: the documents are still there.
|
|
125
|
+
owner = Person.create!
|
|
126
|
+
owner.posts.create!(title: evil_subject)
|
|
127
|
+
|
|
128
|
+
expect do
|
|
129
|
+
owner.posts.delete_all(title: { '$regex' => evil_pattern })
|
|
130
|
+
end.to raise_error(Mongoid::Errors::InMemoryRegexpTimeout)
|
|
131
|
+
|
|
132
|
+
expect(Post.count).to eq(1)
|
|
133
|
+
expect(owner.reload.posts.size).to eq(1)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
context 'when the association being removed from has not been loaded' do
|
|
138
|
+
config_override :in_memory_regexp_time_limit, 0.2
|
|
139
|
+
|
|
140
|
+
let!(:owner) do
|
|
141
|
+
person = Person.create!
|
|
142
|
+
person.posts.create!(title: 'Testing')
|
|
143
|
+
Person.find(person._id)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# The query that loads the association, which removal has to run before it
|
|
147
|
+
# has anything to match against.
|
|
148
|
+
let(:load_query) { owner.posts._target._unloaded }
|
|
149
|
+
|
|
150
|
+
# A stall standing in for a slow network. It is longer than the limit, and
|
|
151
|
+
# none of it is time spent on regular expressions.
|
|
152
|
+
before do
|
|
153
|
+
allow(load_query).to receive(:each).and_wrap_original do |original, *args, &block|
|
|
154
|
+
sleep(0.3)
|
|
155
|
+
original.call(*args, &block)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'does not load it, so no query can be counted against the limit' do
|
|
160
|
+
# On the Timeout path a deadline covering the fetch failed a slow query
|
|
161
|
+
# with an error about regular expressions, and the asynchronous exception
|
|
162
|
+
# could land inside the driver's socket read, leaving the connection with
|
|
163
|
+
# unconsumed bytes. Nothing is fetched now: the scan sees only what the
|
|
164
|
+
# association already holds.
|
|
165
|
+
stub_const('Mongoid::Matcher::RegexpBudget::PER_REGEXP_TIMEOUT', false)
|
|
166
|
+
|
|
167
|
+
expect do
|
|
168
|
+
owner.posts.delete_all(title: { '$regex' => '\Azzz' })
|
|
169
|
+
end.not_to raise_error
|
|
170
|
+
|
|
171
|
+
expect(load_query).not_to have_received(:each)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it 'runs no pattern against documents it never loaded' do
|
|
175
|
+
# The counterpart to the referenced-association examples below, where the
|
|
176
|
+
# documents are in memory and the pattern is run against every one of
|
|
177
|
+
# them. Here there is nothing to run it against, and the server applies
|
|
178
|
+
# the same selector for the delete, so nothing is missed by not looking.
|
|
179
|
+
expect(Mongoid::Matcher::RegexpBudget).not_to receive(:match?)
|
|
180
|
+
|
|
181
|
+
owner.posts.delete_all(title: { '$regex' => '\Azzz' })
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it 'still removes the documents that match' do
|
|
185
|
+
expect(owner.posts.delete_all(title: { '$regex' => '\ATest' })).to eq(1)
|
|
186
|
+
expect(Post.count).to eq(0)
|
|
187
|
+
expect(owner.posts.size).to eq(0)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
context 'when removing documents from a referenced association' do
|
|
192
|
+
config_override :in_memory_regexp_time_limit, nil
|
|
193
|
+
|
|
194
|
+
let!(:owner) { Person.create! }
|
|
195
|
+
|
|
196
|
+
before do
|
|
197
|
+
30.times { owner.posts.create!(title: slow_street) }
|
|
198
|
+
Mongoid::Config.in_memory_regexp_time_limit = calibrated_limit
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it 'raises rather than scanning every loaded document' do
|
|
202
|
+
expect do
|
|
203
|
+
owner.posts.delete_all(title: { '$regex' => slow_pattern })
|
|
204
|
+
end.to raise_error(Mongoid::Errors::InMemoryRegexpTimeout)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it 'leaves the documents in place' do
|
|
208
|
+
expect do
|
|
209
|
+
owner.posts.delete_all(title: { '$regex' => slow_pattern })
|
|
210
|
+
end.to raise_error(Mongoid::Errors::InMemoryRegexpTimeout)
|
|
211
|
+
|
|
212
|
+
expect(Post.count).to eq(30)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe 'query operator injection guard' do
|
|
6
|
+
let(:js) { 'this.name == "admin"' }
|
|
7
|
+
|
|
8
|
+
let(:nested_function) do
|
|
9
|
+
{ '$expr' => { '$function' => { 'body' => 'function() { return true; }', 'args' => [], 'lang' => 'js' } } }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
let(:band) { Band.create!(name: 'Depeche Mode') }
|
|
13
|
+
|
|
14
|
+
context 'with default configuration' do
|
|
15
|
+
it 'rejects unsafe operators without any opt-out' do
|
|
16
|
+
expect(Mongoid.allow_unsafe_query_operators).to be false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
context 'when querying with where' do
|
|
20
|
+
it 'rejects a top-level $where' do
|
|
21
|
+
expect { Band.where('$where' => js).first }.to raise_error(Mongoid::Errors::InvalidQuery)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'rejects a $function nested in $expr' do
|
|
25
|
+
expect { Band.where(nested_function).first }.to raise_error(Mongoid::Errors::InvalidQuery)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'permits an ordinary query' do
|
|
29
|
+
expect(Band.where(name: band.name).first).to eq(band)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
context 'when querying with find_by' do
|
|
34
|
+
it 'rejects a top-level $where' do
|
|
35
|
+
expect { Band.find_by('$where' => js) }.to raise_error(Mongoid::Errors::InvalidQuery)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'rejects a $function nested in $expr' do
|
|
39
|
+
expect { Band.find_by(nested_function) }.to raise_error(Mongoid::Errors::InvalidQuery)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
context 'when querying with find_or_create_by' do
|
|
44
|
+
it 'rejects a top-level $where' do
|
|
45
|
+
expect { Band.find_or_create_by('$where' => js) }.to raise_error(Mongoid::Errors::InvalidQuery)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
context 'when querying with find_or_initialize_by' do
|
|
50
|
+
it 'rejects a top-level $where' do
|
|
51
|
+
expect { Band.find_or_initialize_by('$where' => js) }.to raise_error(Mongoid::Errors::InvalidQuery)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
context 'when querying an association with find_or_create_by' do
|
|
56
|
+
it 'rejects a top-level $where' do
|
|
57
|
+
expect do
|
|
58
|
+
band.records.find_or_create_by('$where' => js)
|
|
59
|
+
end.to raise_error(Mongoid::Errors::InvalidQuery)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
context 'when querying with a logical operator' do
|
|
64
|
+
it 'rejects a $where smuggled through or' do
|
|
65
|
+
expect { Band.or('$where' => js).first }.to raise_error(Mongoid::Errors::InvalidQuery)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'rejects a $where smuggled through any_of' do
|
|
69
|
+
expect { Band.any_of('$where' => js).first }.to raise_error(Mongoid::Errors::InvalidQuery)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
context 'when allow_unsafe_query_operators is true' do
|
|
75
|
+
config_override :allow_unsafe_query_operators, true
|
|
76
|
+
|
|
77
|
+
it 'permits a top-level $where' do
|
|
78
|
+
expect(Band.where('$where' => "this.name == '#{band.name}'").first).to eq(band)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'permits a $function nested in $expr' do
|
|
82
|
+
expect { Band.where(nested_function).first }.not_to raise_error
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'permits a $where smuggled through or' do
|
|
86
|
+
expect { Band.or('$where' => js).first }.not_to raise_error
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe Mongoid::Association::EagerLoadable do
|
|
6
|
+
context 'with through associations' do
|
|
7
|
+
before(:all) do
|
|
8
|
+
Object.const_set(:ELPhysician, Class.new do
|
|
9
|
+
include Mongoid::Document
|
|
10
|
+
|
|
11
|
+
store_in collection: 'el_physicians'
|
|
12
|
+
has_many :el_appointments, class_name: 'ELAppointment', inverse_of: :el_physician
|
|
13
|
+
has_many :el_patients, through: :el_appointments,
|
|
14
|
+
class_name: 'ELPatient', source: :el_patient
|
|
15
|
+
end)
|
|
16
|
+
Object.const_set(:ELAppointment, Class.new do
|
|
17
|
+
include Mongoid::Document
|
|
18
|
+
|
|
19
|
+
store_in collection: 'el_appointments'
|
|
20
|
+
belongs_to :el_physician, class_name: 'ELPhysician'
|
|
21
|
+
belongs_to :el_patient, class_name: 'ELPatient'
|
|
22
|
+
end)
|
|
23
|
+
Object.const_set(:ELPatient, Class.new do
|
|
24
|
+
include Mongoid::Document
|
|
25
|
+
|
|
26
|
+
store_in collection: 'el_patients'
|
|
27
|
+
end)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
after(:all) do
|
|
31
|
+
%w[ELPhysician ELAppointment ELPatient].each { |c| Object.send(:remove_const, c) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
before { [ ELPhysician, ELAppointment, ELPatient ].each(&:delete_all) }
|
|
35
|
+
|
|
36
|
+
it 'logs a warning and falls back to preload when eager_load is used with a through association' do
|
|
37
|
+
physician = ELPhysician.create!
|
|
38
|
+
patient = ELPatient.create!
|
|
39
|
+
ELAppointment.create!(el_physician: physician, el_patient: patient)
|
|
40
|
+
|
|
41
|
+
expect(Mongoid.logger).to receive(:warn).with(a_string_including('through'))
|
|
42
|
+
|
|
43
|
+
docs = ELPhysician.eager_load(:el_patients).to_a
|
|
44
|
+
expect(docs.first.el_patients.to_a).to include(patient)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -679,9 +679,9 @@ describe Mongoid::Association::EagerLoadable do
|
|
|
679
679
|
expect(docs.first.posts).to eq([ post ])
|
|
680
680
|
end
|
|
681
681
|
|
|
682
|
-
it 'mentions the offending association and
|
|
682
|
+
it 'mentions the offending association, client, and database in the warning' do
|
|
683
683
|
expect(Mongoid.logger).to receive(:warn).with(
|
|
684
|
-
/posts \(other_cluster
|
|
684
|
+
/posts \(client: other_cluster, database: /
|
|
685
685
|
)
|
|
686
686
|
context.eager_load_with_lookup
|
|
687
687
|
end
|
|
@@ -4524,6 +4524,39 @@ describe Mongoid::Association::Embedded::EmbedsMany::Proxy do
|
|
|
4524
4524
|
expect(reloaded_band).not_to have_key(:labels)
|
|
4525
4525
|
end
|
|
4526
4526
|
end
|
|
4527
|
+
|
|
4528
|
+
context 'when set via raw attribute write in a before_save callback' do
|
|
4529
|
+
after { Band.reset_callbacks(:save) }
|
|
4530
|
+
|
|
4531
|
+
let(:reloaded_band) { Band.collection.find(_id: band._id).first }
|
|
4532
|
+
|
|
4533
|
+
context 'on a new record' do
|
|
4534
|
+
before { Band.before_save { self[:labels] ||= [] } }
|
|
4535
|
+
|
|
4536
|
+
let(:band) { Band.create! }
|
|
4537
|
+
|
|
4538
|
+
it 'persists the empty list' do
|
|
4539
|
+
expect(reloaded_band).to have_key(:labels)
|
|
4540
|
+
expect(reloaded_band[:labels]).to eq []
|
|
4541
|
+
end
|
|
4542
|
+
end
|
|
4543
|
+
|
|
4544
|
+
context 'on an existing record' do
|
|
4545
|
+
# Reload the band from MongoDB so the labels proxy has not been
|
|
4546
|
+
# initialized, which is the scenario that triggers the bug.
|
|
4547
|
+
let(:band) { Band.find(Band.create!._id) }
|
|
4548
|
+
|
|
4549
|
+
before do
|
|
4550
|
+
Band.before_save { self[:labels] ||= [] }
|
|
4551
|
+
band.save!
|
|
4552
|
+
end
|
|
4553
|
+
|
|
4554
|
+
it 'persists the empty list' do
|
|
4555
|
+
expect(reloaded_band).to have_key(:labels)
|
|
4556
|
+
expect(reloaded_band[:labels]).to eq []
|
|
4557
|
+
end
|
|
4558
|
+
end
|
|
4559
|
+
end
|
|
4527
4560
|
end
|
|
4528
4561
|
|
|
4529
4562
|
context 'when using assign_attributes with an already populated array' do
|