mongoid-rspec 4.3.0 → 4.4.0
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/README.md +126 -0
- data/Rakefile +2 -2
- data/lib/matchers/associations.rb +17 -1
- data/lib/matchers/be_covered_query.rb +75 -0
- data/lib/matchers/execute_mongodb_command.rb +127 -0
- data/lib/matchers/have_field.rb +10 -0
- data/lib/matchers/validations/custom_validation_of.rb +10 -10
- data/lib/matchers/validations/exclusion_of.rb +11 -9
- data/lib/matchers/validations/format_of.rb +17 -15
- data/lib/matchers/validations/inclusion_of.rb +11 -9
- data/lib/matchers/validations/length_of.rb +75 -36
- data/lib/matchers/validations/numericality_of.rb +11 -11
- data/lib/matchers/validations/uniqueness_of.rb +17 -17
- data/lib/matchers/validations.rb +20 -11
- data/lib/mongoid/rspec/version.rb +1 -1
- data/lib/mongoid/rspec.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ac3e47b3e606516f4ca239dad1405872db4cd012443a11a9e3c332d47555c27
|
|
4
|
+
data.tar.gz: 8b4d8baff373f7b2c6bc7e194a5a21192c08bdfda17e40693e300f9fd48a111b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 51157b3c68734d119c2880e7d071f52f244322bf517bfd8cab697d0e2f2e0662877b455acf704923dbed3f24034ab341c4c84ad6a9443230370052f805c6db0e
|
|
7
|
+
data.tar.gz: 0ed7c718a2862d4a1bb5abfd03e430dff67b1589612abbf50b2f449dd0584ecaff99b42b8e204e6281eab65f1b404b1bbf710da65550bd4fb369ef157395b241
|
data/README.md
CHANGED
|
@@ -44,6 +44,33 @@ RSpec.configure do |config|
|
|
|
44
44
|
end
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
### Using Mongoid and ActiveRecord with shoulda-matchers
|
|
48
|
+
|
|
49
|
+
Both libraries define matcher names such as `validate_length_of` and `belong_to`, but their chain methods and implementations differ. In an application with both Mongoid and ActiveRecord models, include Mongoid matchers only for a custom `:mongoid_model` type instead of all `:model` examples. Keep Shoulda's ActiveModel and ActiveRecord matchers scoped to `:model`:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
require 'mongoid-rspec'
|
|
53
|
+
require 'shoulda/matchers'
|
|
54
|
+
|
|
55
|
+
RSpec.configure do |config|
|
|
56
|
+
config.include Mongoid::Matchers, type: :mongoid_model
|
|
57
|
+
config.include Shoulda::Matchers::ActiveModel, type: :model
|
|
58
|
+
config.include Shoulda::Matchers::ActiveRecord, type: :model
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
RSpec.describe MongoidAddress, type: :mongoid_model do
|
|
62
|
+
it { is_expected.to validate_length_of(:name).with_maximum(255) }
|
|
63
|
+
it { is_expected.to belong_to(:city).as_inverse_of(:addresses) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
RSpec.describe ActiveRecordAddress, type: :model do
|
|
67
|
+
it { is_expected.to validate_length_of(:name).is_at_most(255) }
|
|
68
|
+
it { is_expected.to belong_to(:city).inverse_of(:addresses) }
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Replace the existing unscoped or `type: :model` inclusion of `Mongoid::Matchers`; do not keep it alongside the scoped configuration. Explicitly tag Mongoid example groups with `type: :mongoid_model`, including those under `spec/models`, where rspec-rails would otherwise infer `:model`. Do not include Shoulda's model matcher modules globally or directly in those Mongoid groups. This avoids depending on module inclusion order and preserves each library's existing matcher API.
|
|
73
|
+
|
|
47
74
|
### Other
|
|
48
75
|
|
|
49
76
|
Add to your `spec_helper.rb` file.
|
|
@@ -207,8 +234,74 @@ RSpec.describe Article, type: :model do
|
|
|
207
234
|
end
|
|
208
235
|
```
|
|
209
236
|
|
|
237
|
+
### execute_mongodb_command
|
|
238
|
+
|
|
239
|
+
Use `execute_mongodb_command` with a block expectation to verify that code issues a MongoDB command. Without an argument, the matcher passes when any command is issued on the block's thread. An expected command matches the entire application command document exactly, not just a subset or query selector; nested String and Symbol keys are normalized to Strings. Driver-added top-level `$db`, `lsid`, and `$clusterTime` fields are excluded from comparison, while all other command fields and values must match. The matcher ignores unrelated background-thread activity and raises `NotImplementedError` when the driver does not expose command monitoring, including the legacy driver used with Mongoid 4.
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
RSpec.describe FooService do
|
|
243
|
+
it 'uses the cached result without querying MongoDB' do
|
|
244
|
+
expect { FooService.recent_foos }.not_to execute_mongodb_command
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
RSpec.describe Article do
|
|
249
|
+
it 'executes the expected MongoDB command' do
|
|
250
|
+
expected_command = {
|
|
251
|
+
find: 'articles',
|
|
252
|
+
filter: { 'title.en' => 'cached' },
|
|
253
|
+
limit: 10,
|
|
254
|
+
sort: { created_at: -1 }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
expect do
|
|
258
|
+
Article.where(title: 'cached').desc(:created_at).limit(10).to_a
|
|
259
|
+
end.to execute_mongodb_command(expected_command)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### be_covered_query
|
|
265
|
+
|
|
266
|
+
Check that MongoDB's winning plan uses an index without fetching documents:
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
class Article
|
|
270
|
+
index({ title: 1, _id: 1 })
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
RSpec.describe Article do
|
|
274
|
+
it { expect(Article.where(title: 'Example').only(:title)).to be_covered_query }
|
|
275
|
+
end
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Mongoid includes `_id` in an inclusive projection by default, so the index must cover it as well. The matcher examines all stages in the winning plan, including nested shard plans, and rejects plans containing `FETCH` or `COLLSCAN`. A covered plan does not guarantee that a query is fast; this matcher checks plan coverage, not execution time.
|
|
279
|
+
|
|
280
|
+
Use `.using_index('title_1__id_1')` or `.using_indexes('first_index', 'second_index')` to additionally require the exact set of index names used by the winning plan. Index names accept nonempty strings or symbols; order and repeated uses across shards do not matter. Rejected plans are ignored, and the query must still be covered.
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
expect(Article.where(title: 'Example').only(:title)).to be_covered_query.using_index('title_1__id_1')
|
|
284
|
+
```
|
|
285
|
+
|
|
210
286
|
### Field Matchers
|
|
211
287
|
|
|
288
|
+
Chain `.readonly` to check fields declared with Mongoid's `attr_readonly`. This checks the declaration, not whether a new document currently allows writes. Like other field matcher options, it applies to every requested field and accepts class or instance subjects.
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
class Article
|
|
292
|
+
include Mongoid::Document
|
|
293
|
+
|
|
294
|
+
field :slug, type: String
|
|
295
|
+
field :external_id, type: String
|
|
296
|
+
attr_readonly :slug, :external_id
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
RSpec.describe Article do
|
|
300
|
+
it { is_expected.to have_field(:slug).of_type(String).readonly }
|
|
301
|
+
it { is_expected.to have_fields(:slug, :external_id).readonly }
|
|
302
|
+
end
|
|
303
|
+
```
|
|
304
|
+
|
|
212
305
|
```ruby
|
|
213
306
|
RSpec.describe Article do
|
|
214
307
|
it { is_expected.to have_field(:published).of_type(Mongoid::Boolean).with_default_value_of(false) }
|
|
@@ -226,6 +319,31 @@ end
|
|
|
226
319
|
|
|
227
320
|
### Association Matchers
|
|
228
321
|
|
|
322
|
+
Use `with_primary_key` to check an association's primary key, including the default `_id`. Like `with_foreign_key`, it accepts a symbol or string and can be chained with other association options:
|
|
323
|
+
|
|
324
|
+
```ruby
|
|
325
|
+
class Author
|
|
326
|
+
include Mongoid::Document
|
|
327
|
+
|
|
328
|
+
field :code, type: String
|
|
329
|
+
has_many :posts, primary_key: :code, foreign_key: :author_code
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
class Post
|
|
333
|
+
include Mongoid::Document
|
|
334
|
+
|
|
335
|
+
belongs_to :author, primary_key: :code, foreign_key: :author_code
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
RSpec.describe Author do
|
|
339
|
+
it { is_expected.to have_many(:posts).with_primary_key(:code).with_foreign_key(:author_code) }
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
RSpec.describe Post do
|
|
343
|
+
it { is_expected.to belong_to(:author).with_primary_key('code') }
|
|
344
|
+
end
|
|
345
|
+
```
|
|
346
|
+
|
|
229
347
|
```ruby
|
|
230
348
|
RSpec.describe User do
|
|
231
349
|
it { is_expected.to have_many(:articles).with_foreign_key(:author_id).ordered_by(:title) }
|
|
@@ -247,6 +365,8 @@ RSpec.describe User do
|
|
|
247
365
|
|
|
248
366
|
it { is_expected.to have_and_belong_to_many(:children) }
|
|
249
367
|
it { is_expected.to have_and_belong_to_many(:children).of_type(User) }
|
|
368
|
+
# nonconflicting alternative when using shoulda-matchers
|
|
369
|
+
it { is_expected.to have_and_belong_to_many_related(:children).of_type(User) }
|
|
250
370
|
end
|
|
251
371
|
|
|
252
372
|
RSpec.describe Profile do
|
|
@@ -300,6 +420,10 @@ end
|
|
|
300
420
|
|
|
301
421
|
### Validation Matchers
|
|
302
422
|
|
|
423
|
+
`validate_length_of` searches all applicable length validators for one matching the requested options. For example, a maximum-length validator added by `has_secure_password` does not hide a separately declared minimum-length validator. Chained length, message, and `to_allow` options must match a single validator; options from separate validators are not combined.
|
|
424
|
+
|
|
425
|
+
Use `validate_length_of(:name).within(1..100).to_allow(nil: true)` to check `allow_nil: true`, or `.to_allow(blank: true)` to check `allow_blank: true`. Both options can be combined, and the full keys `allow_nil` and `allow_blank` are also accepted. Values must be `true` or `false`; `false` requires an explicitly configured `false` (an omitted option does not match). Omitting `to_allow` leaves these options unchecked. Empty hashes, unknown keys, non-boolean values, and duplicate aliases are rejected with `ArgumentError`.
|
|
426
|
+
|
|
303
427
|
```ruby
|
|
304
428
|
RSpec.describe Site do
|
|
305
429
|
it { is_expected.to validate_presence_of(:name) }
|
|
@@ -396,6 +520,8 @@ end
|
|
|
396
520
|
|
|
397
521
|
You're encouraged to contribute to this library. See [CONTRIBUTING](CONTRIBUTING.md) for details.
|
|
398
522
|
|
|
523
|
+
`bundle exec rake` runs the unit suite without third-party integration dependencies. Each integration has its own Appraisal bundle and directory under `spec/integration/`. For example, run `BUNDLE_GEMFILE=gemfiles/shoulda_matchers.gemfile bundle install`, then `BUNDLE_GEMFILE=gemfiles/shoulda_matchers.gemfile bundle exec rspec spec/integration/shoulda_matchers`. To add an integration, define its dependencies in Appraisals, generate its gemfile, add its specs under a matching directory, and add its name to the integration matrix in `.github/workflows/rspec.yml`. CI runs each listed integration in a separate process and bundle.
|
|
524
|
+
|
|
399
525
|
## Copyright and License
|
|
400
526
|
|
|
401
527
|
Copyright (c) 2009-2018 Evan Sagge and Contributors.
|
data/Rakefile
CHANGED
|
@@ -8,11 +8,11 @@ Bundler::GemHelper.install_tasks
|
|
|
8
8
|
require 'rspec/core/rake_task'
|
|
9
9
|
|
|
10
10
|
RSpec::Core::RakeTask.new(:spec) do |spec|
|
|
11
|
-
spec.pattern = './spec/**/*_spec.rb'
|
|
11
|
+
spec.pattern = './spec/unit/**/*_spec.rb'
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
|
15
|
-
spec.pattern = './spec/**/*_spec.rb'
|
|
15
|
+
spec.pattern = './spec/unit/**/*_spec.rb'
|
|
16
16
|
spec.rcov = true
|
|
17
17
|
end
|
|
18
18
|
|
|
@@ -115,6 +115,12 @@ module Mongoid
|
|
|
115
115
|
self
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
+
def with_primary_key(primary_key)
|
|
119
|
+
@association[:primary_key] = primary_key.to_s
|
|
120
|
+
@expectation_message << " using primary key #{@association[:primary_key].inspect}"
|
|
121
|
+
self
|
|
122
|
+
end
|
|
123
|
+
|
|
118
124
|
def with_counter_cache
|
|
119
125
|
@association[:counter_cache] = true
|
|
120
126
|
@expectation_message << " which specifies counter_cache as #{@association[:counter_cache]}"
|
|
@@ -271,6 +277,15 @@ module Mongoid
|
|
|
271
277
|
end
|
|
272
278
|
end
|
|
273
279
|
|
|
280
|
+
if @association[:primary_key]
|
|
281
|
+
if metadata.primary_key != @association[:primary_key]
|
|
282
|
+
@negative_result_message = "#{@positive_result_message} with primary key #{metadata.primary_key.inspect}"
|
|
283
|
+
return false
|
|
284
|
+
else
|
|
285
|
+
@positive_result_message = "#{@positive_result_message} with primary key #{metadata.primary_key.inspect}"
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
274
289
|
if @association[:counter_cache]
|
|
275
290
|
if metadata.counter_cached? != true
|
|
276
291
|
@negative_result_message = "#{@positive_result_message} which did not set counter_cache"
|
|
@@ -371,9 +386,10 @@ module Mongoid
|
|
|
371
386
|
end
|
|
372
387
|
alias have_many have_many_related
|
|
373
388
|
|
|
374
|
-
def
|
|
389
|
+
def have_and_belong_to_many_related(association_name)
|
|
375
390
|
HaveAssociationMatcher.new(association_name, HAS_AND_BELONGS_TO_MANY)
|
|
376
391
|
end
|
|
392
|
+
alias have_and_belong_to_many have_and_belong_to_many_related
|
|
377
393
|
|
|
378
394
|
def belong_to_related(association_name)
|
|
379
395
|
HaveAssociationMatcher.new(association_name, BELONGS_TO)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mongoid
|
|
4
|
+
module Matchers
|
|
5
|
+
def be_covered_query
|
|
6
|
+
BeCoveredQuery.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class BeCoveredQuery
|
|
10
|
+
PLAN_CHILD_KEYS = %w[inputStage inputStages outerStage innerStage queryPlan winningPlan shards].freeze
|
|
11
|
+
|
|
12
|
+
def using_index(name)
|
|
13
|
+
using_indexes(name)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def using_indexes(*names)
|
|
17
|
+
raise ArgumentError, 'index names must be nonempty strings or symbols' unless names.any? && names.all? { |name| (name.is_a?(String) || name.is_a?(Symbol)) && !name.to_s.empty? }
|
|
18
|
+
|
|
19
|
+
@expected_indexes = names.map(&:to_s).uniq.sort
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def matches?(actual)
|
|
24
|
+
@stages = []
|
|
25
|
+
@indexes = []
|
|
26
|
+
return false unless actual.respond_to?(:explain)
|
|
27
|
+
|
|
28
|
+
explain = actual.explain
|
|
29
|
+
planner = value_for(explain, :queryPlanner)
|
|
30
|
+
winning_plan = value_for(planner, :winningPlan)
|
|
31
|
+
return false unless winning_plan
|
|
32
|
+
|
|
33
|
+
collect_plan(winning_plan)
|
|
34
|
+
covered = @stages.include?('IXSCAN') && (@stages & %w[FETCH COLLSCAN]).empty?
|
|
35
|
+
covered && (!@expected_indexes || ((@indexes.uniq - @expected_indexes).empty? && (@expected_indexes - @indexes).empty?))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def description
|
|
39
|
+
desc = +'be a covered query'
|
|
40
|
+
desc << " using indexes #{@expected_indexes.inspect}" if @expected_indexes
|
|
41
|
+
desc
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def failure_message
|
|
45
|
+
detail = @stages.empty? ? 'no winning plan was found' : "winning plan stages were #{@stages.inspect}"
|
|
46
|
+
detail += " and indexes were #{@indexes.uniq.inspect}" if @expected_indexes && @stages.any?
|
|
47
|
+
"Expected query to #{description}, but #{detail}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def failure_message_when_negated
|
|
51
|
+
"Expected query not to #{description}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def value_for(hash, key)
|
|
57
|
+
return unless hash.respond_to?(:[])
|
|
58
|
+
|
|
59
|
+
hash[key.to_s] || hash[key.to_sym]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def collect_plan(plan)
|
|
63
|
+
case plan
|
|
64
|
+
when Hash
|
|
65
|
+
stage = value_for(plan, :stage)
|
|
66
|
+
@stages << stage.to_s if stage
|
|
67
|
+
@indexes << value_for(plan, :indexName) if stage.to_s == 'IXSCAN'
|
|
68
|
+
PLAN_CHILD_KEYS.each { |key| collect_plan(value_for(plan, key)) }
|
|
69
|
+
when Array
|
|
70
|
+
plan.each { |value| collect_plan(value) }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mongoid
|
|
4
|
+
module Matchers
|
|
5
|
+
def execute_mongodb_command(*expected_commands)
|
|
6
|
+
raise ArgumentError, 'execute_mongodb_command accepts at most one command' if expected_commands.length > 1
|
|
7
|
+
|
|
8
|
+
if expected_commands.empty?
|
|
9
|
+
ExecuteMongodbCommand.new
|
|
10
|
+
else
|
|
11
|
+
ExecuteMongodbCommand.new(expected_commands.first)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class ExecuteMongodbCommand
|
|
16
|
+
DRIVER_METADATA_KEYS = %w[$db lsid $clusterTime].freeze
|
|
17
|
+
UNSPECIFIED = Object.new.freeze
|
|
18
|
+
|
|
19
|
+
def initialize(expected_command = UNSPECIFIED)
|
|
20
|
+
@expected_command = expected_command.equal?(UNSPECIFIED) ? nil : normalize_command(expected_command)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def supports_block_expectations?
|
|
24
|
+
true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def matches?(block)
|
|
28
|
+
raise ArgumentError, 'execute_mongodb_command expects a block' unless block
|
|
29
|
+
raise NotImplementedError, 'execute_mongodb_command requires MongoDB driver command monitoring' unless
|
|
30
|
+
defined?(Mongo::Monitoring::Global) && defined?(Mongo::Monitoring::COMMAND)
|
|
31
|
+
raise NotImplementedError, 'execute_mongodb_command requires the Mongoid client registry' unless
|
|
32
|
+
defined?(Mongoid::Clients) && Mongoid::Clients.respond_to?(:clients)
|
|
33
|
+
|
|
34
|
+
@command_executed = false
|
|
35
|
+
topic = Mongo::Monitoring::COMMAND
|
|
36
|
+
global_monitor = Mongo::Monitoring::Global
|
|
37
|
+
subscriber = CommandSubscriber.new(Thread.current, @expected_command, method(:normalize_command)) do
|
|
38
|
+
@command_executed = true
|
|
39
|
+
end
|
|
40
|
+
clients = mongoid_clients
|
|
41
|
+
subscribed = false
|
|
42
|
+
|
|
43
|
+
begin
|
|
44
|
+
clients.each { |client| client.subscribe(topic, subscriber) }
|
|
45
|
+
global_monitor.subscribe(topic, subscriber)
|
|
46
|
+
subscribed = true
|
|
47
|
+
block.call
|
|
48
|
+
ensure
|
|
49
|
+
current_clients = mongoid_clients
|
|
50
|
+
global_monitor.unsubscribe(topic, subscriber) if subscribed
|
|
51
|
+
(clients + current_clients).uniq.each { |client| client.unsubscribe(topic, subscriber) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
@command_executed
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def failure_message
|
|
58
|
+
message = +'Expected block to execute a MongoDB command'
|
|
59
|
+
message << " matching #{@expected_command.inspect}" if @expected_command
|
|
60
|
+
message
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def failure_message_when_negated
|
|
64
|
+
message = +'Expected block not to execute a MongoDB command'
|
|
65
|
+
message << " matching #{@expected_command.inspect}" if @expected_command
|
|
66
|
+
message
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def normalize_command(command)
|
|
72
|
+
raise ArgumentError, 'expected MongoDB command must be a non-empty Hash' unless command.is_a?(Hash) && !command.empty?
|
|
73
|
+
|
|
74
|
+
normalize_document(command, top_level: true)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def normalize_document(document, top_level: false)
|
|
78
|
+
document.each_with_object({}) do |(key, value), normalized|
|
|
79
|
+
normalized_key = normalize_key(key)
|
|
80
|
+
next if top_level && DRIVER_METADATA_KEYS.include?(normalized_key)
|
|
81
|
+
raise ArgumentError, "duplicate MongoDB command key after normalization: #{normalized_key.inspect}" if normalized.key?(normalized_key)
|
|
82
|
+
|
|
83
|
+
normalized[normalized_key] = normalize_value(value)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def normalize_key(key)
|
|
88
|
+
return key if key.is_a?(String)
|
|
89
|
+
return key.to_s if key.is_a?(Symbol)
|
|
90
|
+
|
|
91
|
+
raise ArgumentError, "MongoDB command keys must be Strings or Symbols, got #{key.class}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def normalize_value(value)
|
|
95
|
+
case value
|
|
96
|
+
when Hash then normalize_document(value)
|
|
97
|
+
when Array then value.map { |item| normalize_value(item) }
|
|
98
|
+
else value
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def mongoid_clients
|
|
103
|
+
Mongoid::Clients.clients.values
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
class CommandSubscriber
|
|
107
|
+
def initialize(thread, expected_command, normalizer, &on_command)
|
|
108
|
+
@thread = thread
|
|
109
|
+
@expected_command = expected_command
|
|
110
|
+
@normalizer = normalizer
|
|
111
|
+
@on_command = on_command
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def started(event)
|
|
115
|
+
return unless Thread.current.equal?(@thread)
|
|
116
|
+
|
|
117
|
+
command = @normalizer.call(event.command)
|
|
118
|
+
@on_command.call if @expected_command.nil? || command == @expected_command
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def succeeded(_event); end
|
|
122
|
+
|
|
123
|
+
def failed(_event); end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
data/lib/matchers/have_field.rb
CHANGED
|
@@ -12,6 +12,11 @@ module Mongoid
|
|
|
12
12
|
self
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
def readonly
|
|
16
|
+
@readonly = true
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
15
20
|
def of_type(type)
|
|
16
21
|
@type = type
|
|
17
22
|
self
|
|
@@ -49,6 +54,10 @@ module Mongoid
|
|
|
49
54
|
error << " with alias #{@klass.fields[attr].options[:as]}"
|
|
50
55
|
end
|
|
51
56
|
|
|
57
|
+
if @readonly && !@klass.readonly_attributes.include?(attr)
|
|
58
|
+
error << ' that is not readonly'
|
|
59
|
+
end
|
|
60
|
+
|
|
52
61
|
@errors.push("field #{attr.inspect}#{error}") unless error.blank?
|
|
53
62
|
|
|
54
63
|
if @localized
|
|
@@ -80,6 +89,7 @@ module Mongoid
|
|
|
80
89
|
desc << " of type #{@type.inspect}" if @type
|
|
81
90
|
desc << " with alias #{@field_alias}" if @field_alias
|
|
82
91
|
desc << " with default value of #{@default.inspect}" unless @default.nil?
|
|
92
|
+
desc << ' marked readonly' if @readonly
|
|
83
93
|
desc
|
|
84
94
|
end
|
|
85
95
|
end
|
|
@@ -13,23 +13,23 @@ module Mongoid
|
|
|
13
13
|
self
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def matches?(actual)
|
|
17
|
-
return false unless (@result = super(actual))
|
|
18
|
-
check_custom_validator if @custom_validator
|
|
19
|
-
|
|
20
|
-
@result
|
|
21
|
-
end
|
|
22
|
-
|
|
23
16
|
def description
|
|
24
17
|
options_desc = +"validate field #{@field.inspect}"
|
|
25
|
-
options_desc << " with custom validator #{@custom_validator.name}" if @
|
|
18
|
+
options_desc << " with custom validator #{@custom_validator.name}" if @custom_validator
|
|
26
19
|
options_desc
|
|
27
20
|
end
|
|
28
21
|
|
|
29
22
|
private
|
|
30
23
|
|
|
31
|
-
def
|
|
32
|
-
|
|
24
|
+
def check_validator(actual, validator)
|
|
25
|
+
return false unless (@result = super)
|
|
26
|
+
check_custom_validator(validator) if @custom_validator
|
|
27
|
+
|
|
28
|
+
@result
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def check_custom_validator(validator)
|
|
32
|
+
if validator.is_a? @custom_validator
|
|
33
33
|
@positive_result_message << " with custom validator of type #{@custom_validator.name}"
|
|
34
34
|
else
|
|
35
35
|
@negative_result_message << " with custom validator not of type #{@custom_validator.name}"
|
|
@@ -13,11 +13,19 @@ module Mongoid
|
|
|
13
13
|
self
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def
|
|
17
|
-
|
|
16
|
+
def description
|
|
17
|
+
options_desc = []
|
|
18
|
+
options_desc << " not allowing the ff. values: #{@not_allowed_values}" if @not_allowed_values
|
|
19
|
+
super << options_desc.to_sentence
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def check_validator(actual, validator)
|
|
25
|
+
return false unless (result = super)
|
|
18
26
|
|
|
19
27
|
if @not_allowed_values
|
|
20
|
-
raw_validator_not_allowed_values =
|
|
28
|
+
raw_validator_not_allowed_values = validator.options[:in]
|
|
21
29
|
|
|
22
30
|
validator_not_allowed_values = case raw_validator_not_allowed_values
|
|
23
31
|
when Range then raw_validator_not_allowed_values.to_a
|
|
@@ -35,12 +43,6 @@ module Mongoid
|
|
|
35
43
|
|
|
36
44
|
result
|
|
37
45
|
end
|
|
38
|
-
|
|
39
|
-
def description
|
|
40
|
-
options_desc = []
|
|
41
|
-
options_desc << " not allowing the ff. values: #{@not_allowed_values}" if @not_allowed_values
|
|
42
|
-
super << options_desc.to_sentence
|
|
43
|
-
end
|
|
44
46
|
end
|
|
45
47
|
|
|
46
48
|
def validate_exclusion_of(field)
|
|
@@ -23,20 +23,30 @@ module Mongoid
|
|
|
23
23
|
self
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def
|
|
27
|
-
|
|
26
|
+
def description
|
|
27
|
+
options_desc = []
|
|
28
|
+
options_desc << " with format #{@format.inspect}" if @format
|
|
29
|
+
options_desc << " allowing the value #{@valid_value.inspect}" if @valid_value
|
|
30
|
+
options_desc << " not allowing the value #{@invalid_value.inspect}" if @invalid_value
|
|
31
|
+
super << options_desc.to_sentence
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def check_validator(actual, validator)
|
|
37
|
+
return false unless result = super
|
|
28
38
|
|
|
29
39
|
if @format
|
|
30
|
-
if
|
|
31
|
-
@positive_result_message = @positive_result_message << " with format #{
|
|
40
|
+
if validator.options[:with] == @format
|
|
41
|
+
@positive_result_message = @positive_result_message << " with format #{validator.options[:format].inspect}"
|
|
32
42
|
else
|
|
33
|
-
@negative_result_message = @negative_result_message << " with format #{
|
|
43
|
+
@negative_result_message = @negative_result_message << " with format #{validator.options[:format].inspect}"
|
|
34
44
|
result = false
|
|
35
45
|
end
|
|
36
46
|
end
|
|
37
47
|
|
|
38
48
|
if @valid_value
|
|
39
|
-
if
|
|
49
|
+
if validator.options[:with] =~ @valid_value
|
|
40
50
|
@positive_result_message = @positive_result_message << " with #{@valid_value.inspect} as a valid value"
|
|
41
51
|
else
|
|
42
52
|
@negative_result_message = @negative_result_message << " with #{@valid_value.inspect} as an invalid value"
|
|
@@ -45,7 +55,7 @@ module Mongoid
|
|
|
45
55
|
end
|
|
46
56
|
|
|
47
57
|
if @invalid_value
|
|
48
|
-
if @invalid_value !~
|
|
58
|
+
if @invalid_value !~ validator.options[:with]
|
|
49
59
|
@positive_result_message = @positive_result_message << " with #{@invalid_value.inspect} as an invalid value"
|
|
50
60
|
else
|
|
51
61
|
@negative_result_message = @negative_result_message << " with #{@invalid_value.inspect} as a valid value"
|
|
@@ -55,14 +65,6 @@ module Mongoid
|
|
|
55
65
|
|
|
56
66
|
result
|
|
57
67
|
end
|
|
58
|
-
|
|
59
|
-
def description
|
|
60
|
-
options_desc = []
|
|
61
|
-
options_desc << " with format #{@format.inspect}" if @format
|
|
62
|
-
options_desc << " allowing the value #{@valid_value.inspect}" if @valid_value
|
|
63
|
-
options_desc << " not allowing the value #{@invalid_value.inspect}" if @invalid_value
|
|
64
|
-
super << options_desc.to_sentence
|
|
65
|
-
end
|
|
66
68
|
end
|
|
67
69
|
|
|
68
70
|
def validate_format_of(field)
|
|
@@ -13,11 +13,19 @@ module Mongoid
|
|
|
13
13
|
self
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def
|
|
17
|
-
|
|
16
|
+
def description
|
|
17
|
+
options_desc = []
|
|
18
|
+
options_desc << " allowing these values: #{@allowed_values}" if @allowed_values
|
|
19
|
+
super << options_desc.to_sentence
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def check_validator(actual, validator)
|
|
25
|
+
return false unless result = super
|
|
18
26
|
|
|
19
27
|
if @allowed_values
|
|
20
|
-
raw_validator_allowed_values =
|
|
28
|
+
raw_validator_allowed_values = validator.options[:in]
|
|
21
29
|
|
|
22
30
|
validator_allowed_values = case raw_validator_allowed_values
|
|
23
31
|
when Range then raw_validator_allowed_values.to_a
|
|
@@ -35,12 +43,6 @@ module Mongoid
|
|
|
35
43
|
|
|
36
44
|
result
|
|
37
45
|
end
|
|
38
|
-
|
|
39
|
-
def description
|
|
40
|
-
options_desc = []
|
|
41
|
-
options_desc << " allowing these values: #{@allowed_values}" if @allowed_values
|
|
42
|
-
super << options_desc.to_sentence
|
|
43
|
-
end
|
|
44
46
|
end
|
|
45
47
|
|
|
46
48
|
def validate_inclusion_of(field)
|
|
@@ -6,6 +6,19 @@ module Mongoid
|
|
|
6
6
|
class ValidateLengthOfMatcher < HaveValidationMatcher
|
|
7
7
|
def initialize(name)
|
|
8
8
|
super(name, :length)
|
|
9
|
+
@allowed_options = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_allow(options)
|
|
13
|
+
aliases = { nil: :allow_nil, blank: :allow_blank, allow_nil: :allow_nil, allow_blank: :allow_blank }
|
|
14
|
+
unless options.is_a?(Hash) && options.any? &&
|
|
15
|
+
options.all? { |key, value| aliases.key?(key) && (value.equal?(true) || value.equal?(false)) } &&
|
|
16
|
+
options.keys.map { |key| aliases[key] }.uniq.size == options.size
|
|
17
|
+
raise ArgumentError, 'validate_length_of#to_allow requires a non-empty Hash of nil, blank, allow_nil, or allow_blank keys with boolean values and no duplicate aliases'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
options.each { |key, value| @allowed_options[aliases[key]] = value }
|
|
21
|
+
self
|
|
9
22
|
end
|
|
10
23
|
|
|
11
24
|
def with_maximum(value)
|
|
@@ -32,72 +45,102 @@ module Mongoid
|
|
|
32
45
|
end
|
|
33
46
|
alias is as_exactly
|
|
34
47
|
|
|
35
|
-
def matches?(actual)
|
|
36
|
-
return false unless @result = super(actual)
|
|
37
|
-
|
|
38
|
-
check_maximum if @maximum
|
|
39
|
-
check_minimum if @minimum
|
|
40
|
-
check_range if @within
|
|
41
|
-
check_exact if @is
|
|
42
|
-
|
|
43
|
-
@result
|
|
44
|
-
end
|
|
45
|
-
|
|
46
48
|
def description
|
|
47
49
|
options_desc = []
|
|
48
50
|
options_desc << "with minimum of #{@minimum}" if @minimum
|
|
49
51
|
options_desc << "with maximum of #{@maximum}" if @maximum
|
|
50
52
|
options_desc << "within the range of #{@within}" if @within
|
|
51
53
|
options_desc << "as exactly #{@is}" if @is
|
|
54
|
+
@allowed_options.each { |key, value| options_desc << "with #{key} #{value.inspect}" }
|
|
52
55
|
super << " #{options_desc.to_sentence}"
|
|
53
56
|
end
|
|
54
57
|
|
|
55
58
|
private
|
|
56
59
|
|
|
57
|
-
def
|
|
58
|
-
|
|
60
|
+
def check_validator(actual, validator)
|
|
61
|
+
return false unless @result = super
|
|
62
|
+
|
|
63
|
+
check_maximum(validator) if @maximum
|
|
64
|
+
check_minimum(validator) if @minimum
|
|
65
|
+
check_range(validator) if @within
|
|
66
|
+
check_exact(validator) if @is
|
|
67
|
+
check_allowed_options(validator)
|
|
68
|
+
|
|
69
|
+
@result
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def select_validator(validators)
|
|
73
|
+
validators.detect do |validator|
|
|
74
|
+
(!@minimum || actual_min(validator) == @minimum) &&
|
|
75
|
+
(!@maximum || actual_max(validator) == @maximum) &&
|
|
76
|
+
(!@within || @within.minmax == [actual_min(validator), actual_max(validator)]) &&
|
|
77
|
+
(!@is || validator.options[:is] == @is) &&
|
|
78
|
+
(!@expected_message || validator.options[:message] == @expected_message) &&
|
|
79
|
+
@allowed_options.all? { |key, value| validator.options[key] == value }
|
|
80
|
+
end || super
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def check_allowed_options(validator)
|
|
84
|
+
@allowed_options.each do |key, value|
|
|
85
|
+
actual_value = validator.options[key]
|
|
86
|
+
if actual_value == value
|
|
87
|
+
@positive_result_message << " with #{key} #{value.inspect}"
|
|
88
|
+
else
|
|
89
|
+
@negative_result_message << " with #{key} #{actual_value.inspect}"
|
|
90
|
+
@result = false
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def check_maximum(validator)
|
|
96
|
+
maximum = actual_max(validator)
|
|
97
|
+
if maximum.nil?
|
|
59
98
|
@negative_result_message << ' with no maximum'
|
|
60
99
|
@result = false
|
|
61
|
-
elsif
|
|
100
|
+
elsif maximum == @maximum
|
|
62
101
|
@positive_result_message << " with maximum of #{@maximum}"
|
|
63
102
|
else
|
|
64
|
-
@negative_result_message << " with maximum of #{
|
|
103
|
+
@negative_result_message << " with maximum of #{maximum}"
|
|
65
104
|
@result = false
|
|
66
105
|
end
|
|
67
106
|
end
|
|
68
107
|
|
|
69
|
-
def check_minimum
|
|
70
|
-
|
|
108
|
+
def check_minimum(validator)
|
|
109
|
+
minimum = actual_min(validator)
|
|
110
|
+
if minimum.nil?
|
|
71
111
|
@negative_result_message << ' with no minimum'
|
|
72
112
|
@result = false
|
|
73
|
-
elsif
|
|
113
|
+
elsif minimum == @minimum
|
|
74
114
|
@positive_result_message << " with minimum of #{@minimum}"
|
|
75
115
|
else
|
|
76
|
-
@negative_result_message << " with minimum of #{
|
|
116
|
+
@negative_result_message << " with minimum of #{minimum}"
|
|
77
117
|
@result = false
|
|
78
118
|
end
|
|
79
119
|
end
|
|
80
120
|
|
|
81
|
-
def check_range
|
|
121
|
+
def check_range(validator)
|
|
82
122
|
min, max = @within.minmax
|
|
83
|
-
|
|
84
|
-
|
|
123
|
+
minimum = actual_min(validator)
|
|
124
|
+
maximum = actual_max(validator)
|
|
125
|
+
if !minimum.nil? && maximum.nil?
|
|
126
|
+
@negative_result_message << " with no minimum but with maximum of #{maximum}"
|
|
85
127
|
@result = false
|
|
86
|
-
elsif
|
|
87
|
-
@negative_result_message << " with minimum_of #{
|
|
128
|
+
elsif minimum.nil? && !maximum.nil?
|
|
129
|
+
@negative_result_message << " with minimum_of #{minimum} but no maximum"
|
|
88
130
|
@result = false
|
|
89
|
-
elsif
|
|
131
|
+
elsif minimum.nil? && maximum.nil?
|
|
90
132
|
@negative_result_message << ' with no minimum and maximum'
|
|
91
133
|
@result = false
|
|
92
|
-
elsif
|
|
134
|
+
elsif minimum == min && maximum == max
|
|
93
135
|
@positive_result_message << " within the range of #{@within.inspect}"
|
|
94
136
|
else
|
|
95
|
-
@negative_result_message << " within the range of #{(
|
|
137
|
+
@negative_result_message << " within the range of #{(minimum..maximum).inspect}"
|
|
96
138
|
@result = false
|
|
97
139
|
end
|
|
98
140
|
end
|
|
99
141
|
|
|
100
|
-
def check_exact
|
|
142
|
+
def check_exact(validator)
|
|
143
|
+
actual_is = validator.options[:is]
|
|
101
144
|
if actual_is == @is
|
|
102
145
|
@positive_result_message << " as exactly #{@is}"
|
|
103
146
|
else
|
|
@@ -106,16 +149,12 @@ module Mongoid
|
|
|
106
149
|
end
|
|
107
150
|
end
|
|
108
151
|
|
|
109
|
-
def
|
|
110
|
-
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def actual_min
|
|
114
|
-
@validator.options[:minimum] || (@validator.options[:in] || @validator.options[:within]).try(&:min)
|
|
152
|
+
def actual_min(validator)
|
|
153
|
+
validator.options[:minimum] || (validator.options[:in] || validator.options[:within]).try(&:min)
|
|
115
154
|
end
|
|
116
155
|
|
|
117
|
-
def actual_max
|
|
118
|
-
|
|
156
|
+
def actual_max(validator)
|
|
157
|
+
validator.options[:maximum] || (validator.options[:in] || validator.options[:within]).try(&:max)
|
|
119
158
|
end
|
|
120
159
|
end
|
|
121
160
|
|
|
@@ -38,24 +38,24 @@ module Mongoid
|
|
|
38
38
|
self
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
def
|
|
42
|
-
|
|
41
|
+
def description
|
|
42
|
+
super << options_message(@options)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
protected
|
|
46
|
+
|
|
47
|
+
def check_validator(actual, validator)
|
|
48
|
+
return false unless result = super
|
|
43
49
|
|
|
44
50
|
@options.each do |comparator, expected_value|
|
|
45
|
-
result &= (
|
|
51
|
+
result &= (validator.options[comparator] == expected_value)
|
|
46
52
|
end
|
|
47
53
|
|
|
48
|
-
@positive_result_message <<= options_message(
|
|
49
|
-
@negative_result_message <<= options_message(
|
|
54
|
+
@positive_result_message <<= options_message(validator.options)
|
|
55
|
+
@negative_result_message <<= options_message(validator.options)
|
|
50
56
|
result
|
|
51
57
|
end
|
|
52
58
|
|
|
53
|
-
def description
|
|
54
|
-
super << options_message(@options)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
protected
|
|
58
|
-
|
|
59
59
|
def options_message(options)
|
|
60
60
|
type_msg = []
|
|
61
61
|
comp_msg = []
|
|
@@ -24,16 +24,6 @@ module Mongoid
|
|
|
24
24
|
self
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
def matches?(actual)
|
|
28
|
-
return false unless @result = super(actual)
|
|
29
|
-
|
|
30
|
-
check_scope if @scope
|
|
31
|
-
check_allow_blank if @allow_blank
|
|
32
|
-
check_case_sensitivity if @case_insensitive
|
|
33
|
-
|
|
34
|
-
@result
|
|
35
|
-
end
|
|
36
|
-
|
|
37
27
|
def description
|
|
38
28
|
options_desc = []
|
|
39
29
|
options_desc << " scoped to #{@scope.inspect}" if @scope
|
|
@@ -44,8 +34,18 @@ module Mongoid
|
|
|
44
34
|
|
|
45
35
|
private
|
|
46
36
|
|
|
47
|
-
def
|
|
48
|
-
|
|
37
|
+
def check_validator(actual, validator)
|
|
38
|
+
return false unless @result = super
|
|
39
|
+
|
|
40
|
+
check_scope(validator) if @scope
|
|
41
|
+
check_allow_blank(validator) if @allow_blank
|
|
42
|
+
check_case_sensitivity(validator) if @case_insensitive
|
|
43
|
+
|
|
44
|
+
@result
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def check_allow_blank(validator)
|
|
48
|
+
if validator.options[:allow_blank] == @allow_blank
|
|
49
49
|
@positive_result_message << ' with blank values allowed'
|
|
50
50
|
else
|
|
51
51
|
@negative_result_message << ' with no blank values allowed'
|
|
@@ -53,9 +53,9 @@ module Mongoid
|
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
def check_scope
|
|
57
|
-
message = " scope to #{
|
|
58
|
-
if [
|
|
56
|
+
def check_scope(validator)
|
|
57
|
+
message = " scope to #{validator.options[:scope]}"
|
|
58
|
+
if [validator.options[:scope]].flatten.map(&:to_sym) == @scope
|
|
59
59
|
@positive_result_message << message
|
|
60
60
|
else
|
|
61
61
|
@negative_result_message << message
|
|
@@ -63,8 +63,8 @@ module Mongoid
|
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
def check_case_sensitivity
|
|
67
|
-
if
|
|
66
|
+
def check_case_sensitivity(validator)
|
|
67
|
+
if validator.options[:case_sensitive] == false
|
|
68
68
|
@positive_result_message << ' with case insensitive values'
|
|
69
69
|
else
|
|
70
70
|
@negative_result_message << ' without case insensitive values'
|
data/lib/matchers/validations.rb
CHANGED
|
@@ -13,22 +13,20 @@ module Mongoid
|
|
|
13
13
|
def matches?(actual)
|
|
14
14
|
@klass = actual.is_a?(Class) ? actual : actual.class
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
validators = @klass.validators_on(@field).lazy.select do |v|
|
|
17
17
|
(v.kind.to_s == @type) && (!v.options[:on] || on_options_matches?(v)) &&
|
|
18
18
|
if_condition_matches?(actual, v) && unless_condition_matches?(actual, v)
|
|
19
19
|
end
|
|
20
|
+
validator = select_validator(validators)
|
|
20
21
|
|
|
21
|
-
if
|
|
22
|
+
if validator
|
|
22
23
|
@negative_result_message = +"#{@type.inspect} validator on #{@field.inspect}"
|
|
23
24
|
@positive_result_message = +"#{@type.inspect} validator on #{@field.inspect}"
|
|
24
25
|
else
|
|
25
26
|
@negative_result_message = +"no #{@type.inspect} validator on #{@field.inspect}"
|
|
26
27
|
return false
|
|
27
28
|
end
|
|
28
|
-
|
|
29
|
-
check_on if @options[:on]
|
|
30
|
-
check_expected_message if @expected_message
|
|
31
|
-
@result
|
|
29
|
+
check_validator(actual, validator)
|
|
32
30
|
end
|
|
33
31
|
|
|
34
32
|
def failure_message_for_should
|
|
@@ -66,6 +64,17 @@ module Mongoid
|
|
|
66
64
|
|
|
67
65
|
private
|
|
68
66
|
|
|
67
|
+
def check_validator(_actual, validator)
|
|
68
|
+
@result = true
|
|
69
|
+
check_on(validator) if @options[:on]
|
|
70
|
+
check_expected_message(validator) if @expected_message
|
|
71
|
+
@result
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def select_validator(validators)
|
|
75
|
+
validators.first
|
|
76
|
+
end
|
|
77
|
+
|
|
69
78
|
def if_condition_matches?(actual, validator)
|
|
70
79
|
return true unless @check_conditions
|
|
71
80
|
return true unless validator.options[:if]
|
|
@@ -95,13 +104,13 @@ module Mongoid
|
|
|
95
104
|
end
|
|
96
105
|
end
|
|
97
106
|
|
|
98
|
-
def check_on
|
|
99
|
-
validator_on_methods = [
|
|
107
|
+
def check_on(validator)
|
|
108
|
+
validator_on_methods = [validator.options[:on]].flatten
|
|
100
109
|
|
|
101
110
|
if validator_on_methods.any?
|
|
102
111
|
message = +" on methods: #{validator_on_methods}"
|
|
103
112
|
|
|
104
|
-
if on_options_covered_by?(
|
|
113
|
+
if on_options_covered_by?(validator)
|
|
105
114
|
@positive_result_message << message
|
|
106
115
|
else
|
|
107
116
|
@negative_result_message << message
|
|
@@ -118,8 +127,8 @@ module Mongoid
|
|
|
118
127
|
([@options[:on]].flatten - [validator.options[:on]].flatten).empty?
|
|
119
128
|
end
|
|
120
129
|
|
|
121
|
-
def check_expected_message
|
|
122
|
-
actual_message =
|
|
130
|
+
def check_expected_message(validator)
|
|
131
|
+
actual_message = validator.options[:message]
|
|
123
132
|
if actual_message.nil?
|
|
124
133
|
@negative_result_message << ' with no custom message'
|
|
125
134
|
@result = false
|
data/lib/mongoid/rspec.rb
CHANGED
|
@@ -31,7 +31,9 @@ require 'matchers/validations/absence_of'
|
|
|
31
31
|
require 'matchers/be_mongoid_document'
|
|
32
32
|
require 'matchers/be_dynamic_document'
|
|
33
33
|
require 'matchers/be_stored_in'
|
|
34
|
+
require 'matchers/be_covered_query'
|
|
34
35
|
require 'matchers/have_field'
|
|
36
|
+
require 'matchers/execute_mongodb_command'
|
|
35
37
|
require 'matchers/indexes/have_index_for'
|
|
36
38
|
if Mongoid::Compatibility::Version.mongoid3?
|
|
37
39
|
require 'matchers/indexes/v3/have_index_for'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mongoid-rspec
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Evan Sagge
|
|
@@ -142,9 +142,11 @@ files:
|
|
|
142
142
|
- lib/matchers/accept_nested_attributes.rb
|
|
143
143
|
- lib/matchers/allow_mass_assignment.rb
|
|
144
144
|
- lib/matchers/associations.rb
|
|
145
|
+
- lib/matchers/be_covered_query.rb
|
|
145
146
|
- lib/matchers/be_dynamic_document.rb
|
|
146
147
|
- lib/matchers/be_mongoid_document.rb
|
|
147
148
|
- lib/matchers/be_stored_in.rb
|
|
149
|
+
- lib/matchers/execute_mongodb_command.rb
|
|
148
150
|
- lib/matchers/have_field.rb
|
|
149
151
|
- lib/matchers/have_timestamps.rb
|
|
150
152
|
- lib/matchers/indexes/have_index_for.rb
|