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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/lib/config/locales/en.yml +49 -0
  3. data/lib/mongoid/association/accessors.rb +9 -1
  4. data/lib/mongoid/association/depending.rb +14 -10
  5. data/lib/mongoid/association/eager.rb +9 -0
  6. data/lib/mongoid/association/eager_load/discriminated_inclusion.rb +88 -0
  7. data/lib/mongoid/association/eager_load/embedded_distributor.rb +151 -0
  8. data/lib/mongoid/association/eager_load/inclusion.rb +173 -0
  9. data/lib/mongoid/association/eager_load/inclusion_tree.rb +96 -0
  10. data/lib/mongoid/association/eager_load/lookup_pipeline.rb +121 -0
  11. data/lib/mongoid/association/eager_load/polymorphic_preloader.rb +61 -0
  12. data/lib/mongoid/association/eager_load/polymorphic_targets.rb +151 -0
  13. data/lib/mongoid/association/eager_loadable.rb +57 -100
  14. data/lib/mongoid/association/embedded/embeds_many/proxy.rb +6 -1
  15. data/lib/mongoid/association/embedded/embeds_one.rb +1 -0
  16. data/lib/mongoid/association/macros.rb +13 -1
  17. data/lib/mongoid/association/nested/many.rb +34 -5
  18. data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
  19. data/lib/mongoid/association/nested/one.rb +6 -1
  20. data/lib/mongoid/association/options.rb +17 -0
  21. data/lib/mongoid/association/referenced/belongs_to/eager.rb +6 -1
  22. data/lib/mongoid/association/referenced/belongs_to.rb +1 -0
  23. data/lib/mongoid/association/referenced/counter_cache.rb +3 -3
  24. data/lib/mongoid/association/referenced/has_and_belongs_to_many.rb +5 -0
  25. data/lib/mongoid/association/referenced/has_many/enumerable.rb +3 -1
  26. data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
  27. data/lib/mongoid/association/referenced/has_many_through/eager.rb +99 -0
  28. data/lib/mongoid/association/referenced/has_many_through/proxy.rb +68 -0
  29. data/lib/mongoid/association/referenced/has_many_through.rb +201 -0
  30. data/lib/mongoid/association/referenced/has_one.rb +1 -0
  31. data/lib/mongoid/association/referenced/has_one_through/eager.rb +65 -0
  32. data/lib/mongoid/association/referenced/has_one_through/proxy.rb +30 -0
  33. data/lib/mongoid/association/referenced/has_one_through.rb +167 -0
  34. data/lib/mongoid/association/referenced.rb +2 -0
  35. data/lib/mongoid/association/relatable.rb +40 -0
  36. data/lib/mongoid/association.rb +7 -0
  37. data/lib/mongoid/clients/factory.rb +7 -13
  38. data/lib/mongoid/collection_configurable.rb +8 -0
  39. data/lib/mongoid/config/defaults.rb +0 -1
  40. data/lib/mongoid/config/encryption.rb +36 -19
  41. data/lib/mongoid/config.rb +56 -5
  42. data/lib/mongoid/contextual/aggregable/memory.rb +6 -2
  43. data/lib/mongoid/contextual/memory.rb +18 -7
  44. data/lib/mongoid/criteria/includable.rb +35 -11
  45. data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
  46. data/lib/mongoid/criteria/queryable/selectable.rb +123 -7
  47. data/lib/mongoid/deprecable.rb +45 -2
  48. data/lib/mongoid/encryptable.rb +46 -0
  49. data/lib/mongoid/errors/config_redactor.rb +43 -0
  50. data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
  51. data/lib/mongoid/errors/mixed_client_configuration.rb +1 -1
  52. data/lib/mongoid/errors/no_client_database.rb +1 -1
  53. data/lib/mongoid/errors/no_client_hosts.rb +1 -1
  54. data/lib/mongoid/errors/no_encryption_schema.rb +28 -0
  55. data/lib/mongoid/errors/readonly_association.rb +29 -0
  56. data/lib/mongoid/errors.rb +4 -0
  57. data/lib/mongoid/extensions/vector.rb +64 -0
  58. data/lib/mongoid/extensions.rb +1 -0
  59. data/lib/mongoid/field_readable.rb +69 -0
  60. data/lib/mongoid/indexable.rb +38 -26
  61. data/lib/mongoid/matchable.rb +6 -1
  62. data/lib/mongoid/matcher/eq_impl_with_regexp.rb +2 -4
  63. data/lib/mongoid/matcher/regex.rb +11 -12
  64. data/lib/mongoid/matcher/regexp_budget.rb +372 -0
  65. data/lib/mongoid/matcher.rb +1 -0
  66. data/lib/mongoid/persistable/creatable.rb +1 -1
  67. data/lib/mongoid/persistable/updatable.rb +9 -11
  68. data/lib/mongoid/persistence_context.rb +35 -0
  69. data/lib/mongoid/search_indexable.rb +64 -23
  70. data/lib/mongoid/serializable.rb +12 -4
  71. data/lib/mongoid/tasks/database.rb +23 -9
  72. data/lib/mongoid/threaded.rb +36 -0
  73. data/lib/mongoid/timestamps/timeless.rb +84 -10
  74. data/lib/mongoid/traversable.rb +2 -2
  75. data/lib/mongoid/validatable/associated.rb +18 -2
  76. data/lib/mongoid/version.rb +1 -1
  77. data/spec/integration/app_spec.rb +7 -1
  78. data/spec/integration/associations/belongs_to_spec.rb +13 -0
  79. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
  80. data/spec/integration/dots_and_dollars_spec.rb +12 -2
  81. data/spec/integration/encryption_spec.rb +151 -0
  82. data/spec/integration/matcher_operator_data/regex.yml +21 -0
  83. data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
  84. data/spec/integration/query_operator_guard_spec.rb +89 -0
  85. data/spec/mongoid/association/eager_loadable_spec.rb +47 -0
  86. data/spec/mongoid/association/eager_spec.rb +2 -2
  87. data/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +33 -0
  88. data/spec/mongoid/association/fallback_spec.rb +425 -0
  89. data/spec/mongoid/association/macros_spec.rb +26 -0
  90. data/spec/mongoid/association/referenced/belongs_to/eager_spec.rb +17 -0
  91. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
  92. data/spec/mongoid/association/referenced/has_and_belongs_to_many_spec.rb +50 -0
  93. data/spec/mongoid/association/referenced/has_many/eager_spec.rb +33 -0
  94. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
  95. data/spec/mongoid/association/referenced/has_many_through/eager_spec.rb +58 -0
  96. data/spec/mongoid/association/referenced/has_many_through/proxy_spec.rb +62 -0
  97. data/spec/mongoid/association/referenced/has_many_through_spec.rb +399 -0
  98. data/spec/mongoid/association/referenced/has_one_through/eager_spec.rb +58 -0
  99. data/spec/mongoid/association/referenced/has_one_through/proxy_spec.rb +49 -0
  100. data/spec/mongoid/association/referenced/has_one_through_spec.rb +211 -0
  101. data/spec/mongoid/attributes/nested_spec.rb +249 -8
  102. data/spec/mongoid/config/defaults_spec.rb +33 -0
  103. data/spec/mongoid/config/encryption_spec.rb +228 -0
  104. data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
  105. data/spec/mongoid/contextual/memory_spec.rb +177 -0
  106. data/spec/mongoid/criteria/includable_spec.rb +875 -0
  107. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +15 -1
  108. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +201 -0
  109. data/spec/mongoid/criteria_spec.rb +3 -0
  110. data/spec/mongoid/encryptable_spec.rb +61 -0
  111. data/spec/mongoid/errors/config_redactor_spec.rb +112 -0
  112. data/spec/mongoid/errors/mixed_client_configuration_spec.rb +31 -0
  113. data/spec/mongoid/errors/no_client_database_spec.rb +29 -0
  114. data/spec/mongoid/errors/no_client_hosts_spec.rb +26 -0
  115. data/spec/mongoid/errors/readonly_association_spec.rb +34 -0
  116. data/spec/mongoid/extensions/vector_spec.rb +175 -0
  117. data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
  118. data/spec/mongoid/search_indexable_spec.rb +295 -9
  119. data/spec/mongoid/tasks/database_spec.rb +18 -0
  120. data/spec/mongoid/timestamps/timeless_spec.rb +197 -5
  121. data/spec/mongoid/touchable_spec.rb +25 -0
  122. data/spec/mongoid/touchable_spec_models.rb +10 -0
  123. data/spec/mongoid/validatable/associated_spec.rb +19 -0
  124. data/spec/spec_helper.rb +5 -1
  125. data/spec/support/crypt/models.rb +157 -0
  126. metadata +52 -2
@@ -1589,7 +1589,14 @@ describe Mongoid::Criteria::Queryable::Selectable do
1589
1589
  end
1590
1590
 
1591
1591
  describe '#not' do
1592
+ # deprecated functionality
1592
1593
  context 'when provided no criterion' do
1594
+ around do |example|
1595
+ Mongoid.deprecator.silence do
1596
+ example.run
1597
+ end
1598
+ end
1599
+
1593
1600
  let(:selection) do
1594
1601
  query.not
1595
1602
  end
@@ -1725,6 +1732,9 @@ describe Mongoid::Criteria::Queryable::Selectable do
1725
1732
  end
1726
1733
 
1727
1734
  context 'when the following criteria uses string were form' do
1735
+ # String criteria compile to $where, which requires the opt-in.
1736
+ config_override :allow_unsafe_query_operators, true
1737
+
1728
1738
  let(:selection) do
1729
1739
  query.not.where('hello world')
1730
1740
  end
@@ -1855,9 +1865,13 @@ describe Mongoid::Criteria::Queryable::Selectable do
1855
1865
  it_behaves_like 'returns a cloned query'
1856
1866
  end
1857
1867
 
1868
+ # This context tests a deprecated syntax and behavior, and must be
1869
+ # removed when the deprecated parameterless-not syntax is removed.
1858
1870
  context 'when the criterion are a double negative' do
1859
1871
  let(:selection) do
1860
- query.not.where(:first.not => /1/)
1872
+ Mongoid.deprecator.silence do
1873
+ query.not.where(:first.not => /1/)
1874
+ end
1861
1875
  end
1862
1876
 
1863
1877
  it 'does not double the $not selector' do
@@ -32,6 +32,9 @@ describe Mongoid::Criteria::Queryable::Selectable do
32
32
  it_behaves_like 'requires a non-nil argument'
33
33
 
34
34
  context 'when provided a string' do
35
+ # String criteria compile to $where, which requires the opt-in.
36
+ config_override :allow_unsafe_query_operators, true
37
+
35
38
  let(:selection) do
36
39
  query.where('this.value = 10')
37
40
  end
@@ -550,4 +553,202 @@ describe Mongoid::Criteria::Queryable::Selectable do
550
553
  end
551
554
  end
552
555
  end
556
+
557
+ describe 'top-level operator injection guard' do
558
+ context 'when allow_unsafe_query_operators is true' do
559
+ config_override :allow_unsafe_query_operators, true
560
+
561
+ context 'when passing $where' do
562
+ it 'does not raise' do
563
+ expect do
564
+ query.where('$where' => 'this.name == "admin"')
565
+ end.not_to raise_error
566
+ end
567
+ end
568
+
569
+ context 'when passing $function' do
570
+ it 'does not raise' do
571
+ expect do
572
+ query.where('$function' => { body: 'function() { return true; }', args: [], lang: 'js' })
573
+ end.not_to raise_error
574
+ end
575
+ end
576
+
577
+ context 'when passing a string criterion' do
578
+ it 'does not raise' do
579
+ expect do
580
+ query.where('this.name == "admin"')
581
+ end.not_to raise_error
582
+ end
583
+ end
584
+
585
+ context 'when passing a string criterion to a negated query' do
586
+ it 'does not raise' do
587
+ expect do
588
+ query.not.where('this.name == "admin"')
589
+ end.not_to raise_error
590
+ end
591
+ end
592
+ end
593
+
594
+ context 'when allow_unsafe_query_operators is false' do
595
+ config_override :allow_unsafe_query_operators, false
596
+
597
+ context 'when passing $where' do
598
+ it 'raises InvalidQuery' do
599
+ expect do
600
+ query.where('$where' => 'this.name == "admin"')
601
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /\$where/)
602
+ end
603
+ end
604
+
605
+ context 'when passing $function' do
606
+ it 'raises InvalidQuery' do
607
+ expect do
608
+ query.where('$function' => { body: 'function() { return true; }', args: [], lang: 'js' })
609
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /\$function/)
610
+ end
611
+ end
612
+
613
+ context 'when the error mentions allow_unsafe_query_operators' do
614
+ it 'includes the config opt-in in the message' do
615
+ expect do
616
+ query.where('$where' => 'true')
617
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /allow_unsafe_query_operators/)
618
+ end
619
+ end
620
+
621
+ context 'when passing a string criterion' do
622
+ it 'raises InvalidQuery' do
623
+ expect do
624
+ query.where('this.name == "admin"')
625
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /\$where/)
626
+ end
627
+
628
+ it 'includes the config opt-in in the message' do
629
+ expect do
630
+ query.where('this.name == "admin"')
631
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /allow_unsafe_query_operators/)
632
+ end
633
+ end
634
+
635
+ context 'when passing a string criterion to a negated query' do
636
+ it 'raises InvalidQuery' do
637
+ expect do
638
+ query.not.where('this.name == "admin"')
639
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /\$where/)
640
+ end
641
+ end
642
+
643
+ %w[$and $or $nor $not $text $comment $expr $jsonSchema $alwaysFalse $alwaysTrue].each do |op|
644
+ context "when passing #{op} (allowlisted)" do
645
+ it 'does not raise' do
646
+ value = case op
647
+ when '$and', '$or', '$nor', '$not' then [ { 'x' => 1 } ]
648
+ when '$text' then { '$search' => 'hi' }
649
+ when '$expr' then { '$gt' => [ '$a', 1 ] }
650
+ when '$jsonSchema' then { 'required' => [ 'x' ] }
651
+ else true
652
+ end
653
+ expect { query.where(op => value) }.not_to raise_error
654
+ end
655
+ end
656
+ end
657
+ end
658
+ end
659
+
660
+ describe 'nested operator injection guard' do
661
+ js = 'this.name == "admin"'
662
+
663
+ function_expr = {
664
+ '$function' => { 'body' => 'function() { return true; }', 'args' => [], 'lang' => 'js' }
665
+ }.freeze
666
+
667
+ accumulator_expr = {
668
+ '$accumulator' => {
669
+ 'init' => 'function() { return 0; }',
670
+ 'accumulate' => 'function() { return 0; }',
671
+ 'accumulateArgs' => [],
672
+ 'merge' => 'function() { return 0; }',
673
+ 'lang' => 'js'
674
+ }
675
+ }.freeze
676
+
677
+ # Every path into the selector that does not route through #expr_query,
678
+ # plus the nested forms that a top-level key check cannot see.
679
+ unsafe_queries = {
680
+ 'and with a $where key' => ->(query) { query.and('$where' => js) },
681
+ 'or with a $where key' => ->(query) { query.or('$where' => js) },
682
+ 'nor with a $where key' => ->(query) { query.nor('$where' => js) },
683
+ 'not with a $where key' => ->(query) { query.not('$where' => js) },
684
+ 'any_of with a $where key' => ->(query) { query.any_of('$where' => js) },
685
+ 'none_of with a $where key' => ->(query) { query.none_of('$where' => js) },
686
+ 'elem_match with a nested $where' => ->(query) { query.elem_match(a: { '$where' => js }) },
687
+ 'where with $where inside $or' => ->(query) { query.where('$or' => [ { '$where' => js } ]) },
688
+ 'where with $function inside $expr' => ->(query) { query.where('$expr' => function_expr) },
689
+ 'where with $accumulator inside $expr' => ->(query) { query.where('$expr' => accumulator_expr) },
690
+ 'where with $where three levels deep' => lambda { |query|
691
+ query.where('$and' => [ { '$or' => [ { '$where' => js } ] } ])
692
+ },
693
+ 'where with a symbol $where key inside $or' => ->(query) { query.where('$or' => [ { :$where => js } ]) }
694
+ }.freeze
695
+
696
+ context 'when allow_unsafe_query_operators is false' do
697
+ config_override :allow_unsafe_query_operators, false
698
+
699
+ unsafe_queries.each do |description, builder|
700
+ context "when querying with #{description}" do
701
+ it 'raises InvalidQuery' do
702
+ expect do
703
+ builder.call(query)
704
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /\$where|\$function|\$accumulator/)
705
+ end
706
+ end
707
+ end
708
+
709
+ it 'includes the config opt-in in the message' do
710
+ expect do
711
+ query.or('$where' => js)
712
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /allow_unsafe_query_operators/)
713
+ end
714
+
715
+ context 'when the nested expression is safe' do
716
+ it 'permits $or with field expressions' do
717
+ expect do
718
+ query.where('$or' => [ { 'a' => 1 }, { 'b' => { '$gt' => 2 } } ])
719
+ end.not_to raise_error
720
+ end
721
+
722
+ it 'permits $expr with aggregation operators' do
723
+ expect do
724
+ query.where('$expr' => { '$gt' => [ '$a', '$b' ] })
725
+ end.not_to raise_error
726
+ end
727
+
728
+ it 'permits a field whose name resembles a javascript operator' do
729
+ expect do
730
+ query.where('where' => js)
731
+ end.not_to raise_error
732
+ end
733
+
734
+ it 'permits a value that resembles a javascript operator' do
735
+ expect do
736
+ query.where('name' => '$where')
737
+ end.not_to raise_error
738
+ end
739
+ end
740
+ end
741
+
742
+ context 'when allow_unsafe_query_operators is true' do
743
+ config_override :allow_unsafe_query_operators, true
744
+
745
+ unsafe_queries.each do |description, builder|
746
+ context "when querying with #{description}" do
747
+ it 'does not raise' do
748
+ expect { builder.call(query) }.not_to raise_error
749
+ end
750
+ end
751
+ end
752
+ end
753
+ end
553
754
  end
@@ -2440,6 +2440,9 @@ describe Mongoid::Criteria do
2440
2440
  end
2441
2441
 
2442
2442
  context 'when the criteria is not embedded' do
2443
+ # String criteria compile to $where, which requires the opt-in.
2444
+ config_override :allow_unsafe_query_operators, true
2445
+
2443
2446
  let(:criteria) do
2444
2447
  Band.where("this.name == 'Depeche Mode'")
2445
2448
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'support/crypt/models'
5
+
6
+ describe Mongoid::Encryptable do
7
+ describe '.requires_encryption_schema?' do
8
+ context 'when the model declares encrypt_with' do
9
+ it 'returns true' do
10
+ expect(Crypt::Vault.requires_encryption_schema?).to be true
11
+ end
12
+ end
13
+
14
+ context 'when the model has an encrypted field' do
15
+ it 'returns true' do
16
+ expect(Crypt::User.requires_encryption_schema?).to be true
17
+ end
18
+ end
19
+
20
+ context 'when the model embeds an encrypted model' do
21
+ it 'returns true' do
22
+ expect(Crypt::Wallet.requires_encryption_schema?).to be true
23
+ end
24
+ end
25
+
26
+ context 'when the encrypted model is nested three levels deep' do
27
+ it 'returns true' do
28
+ expect(Crypt::Owner.requires_encryption_schema?).to be true
29
+ end
30
+ end
31
+
32
+ context 'when the model declares no encryption anywhere' do
33
+ it 'returns false' do
34
+ expect(Person.requires_encryption_schema?).to be false
35
+ end
36
+ end
37
+
38
+ # An association target does not have to be a Mongoid document. Truck
39
+ # embeds a plain Ruby class.
40
+ context 'when an embedded association target is not a document' do
41
+ it 'returns false' do
42
+ expect(Truck.requires_encryption_schema?).to be false
43
+ end
44
+ end
45
+
46
+ context 'when the model embeds itself' do
47
+ it 'terminates' do
48
+ expect(Crypt::Comment.requires_encryption_schema?).to be true
49
+ end
50
+ end
51
+
52
+ # Every model is asked this question, including ones naming an embedded
53
+ # class that is never defined. Such an association cannot be used, so
54
+ # nothing is embedded through it and nothing needs encrypting.
55
+ context 'when an embedded association names a class that is not defined' do
56
+ it 'returns false' do
57
+ expect(Crypt::Drawer.requires_encryption_schema?).to be false
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Mongoid::Errors::ConfigRedactor do
6
+ describe '.redact' do
7
+ subject(:redacted) { described_class.redact(config) }
8
+
9
+ context 'when the config is not a hash' do
10
+ let(:config) { 'not a hash' }
11
+
12
+ it 'returns the value unchanged' do
13
+ expect(redacted).to eq('not a hash')
14
+ end
15
+ end
16
+
17
+ context 'when the config has a :password key' do
18
+ let(:config) { { hosts: [ 'localhost:27017' ], password: 's3cr3t' } }
19
+
20
+ it 'replaces the password value' do
21
+ expect(redacted[:password]).to eq('[REDACTED]')
22
+ end
23
+
24
+ it 'leaves other values intact' do
25
+ expect(redacted[:hosts]).to eq([ 'localhost:27017' ])
26
+ end
27
+ end
28
+
29
+ context 'when the config has a string "password" key' do
30
+ let(:config) { { 'password' => 's3cr3t' } }
31
+
32
+ it 'replaces the password value' do
33
+ expect(redacted['password']).to eq('[REDACTED]')
34
+ end
35
+ end
36
+
37
+ context 'when the config has a :uri with embedded credentials' do
38
+ let(:config) { { uri: 'mongodb://admin:s3cr3t@cluster.example.com/db' } }
39
+
40
+ it 'strips the userinfo from the URI' do
41
+ expect(redacted[:uri]).to eq('mongodb://[REDACTED]@cluster.example.com/db')
42
+ end
43
+ end
44
+
45
+ context 'when the config has a mongodb+srv URI' do
46
+ let(:config) { { uri: 'mongodb+srv://admin:s3cr3t@cluster.example.com/db' } }
47
+
48
+ it 'strips the userinfo from the URI' do
49
+ expect(redacted[:uri]).to eq('mongodb+srv://[REDACTED]@cluster.example.com/db')
50
+ end
51
+ end
52
+
53
+ context 'when the URI has no userinfo' do
54
+ let(:config) { { uri: 'mongodb://cluster.example.com/db' } }
55
+
56
+ it 'leaves the URI unchanged' do
57
+ expect(redacted[:uri]).to eq('mongodb://cluster.example.com/db')
58
+ end
59
+ end
60
+
61
+ context 'when the URI value is not a string' do
62
+ let(:config) { { uri: nil } }
63
+
64
+ it 'leaves the value unchanged' do
65
+ expect(redacted[:uri]).to be_nil
66
+ end
67
+ end
68
+
69
+ context 'when the config has nested auto_encryption_options' do
70
+ let(:kms) do
71
+ {
72
+ aws: { access_key_id: 'AKIA...', secret_access_key: 'secret-value' },
73
+ local: { key: 'A' * 96 }
74
+ }
75
+ end
76
+ let(:config) do
77
+ {
78
+ database: 'mongoid_test',
79
+ options: {
80
+ auto_encryption_options: {
81
+ key_vault_namespace: 'admin.datakeys',
82
+ kms_providers: kms
83
+ }
84
+ }
85
+ }
86
+ end
87
+
88
+ it 'redacts the entire auto_encryption_options value' do
89
+ expect(redacted[:options][:auto_encryption_options]).to eq('[REDACTED]')
90
+ end
91
+
92
+ it 'preserves sibling values in the parent hash' do
93
+ expect(redacted[:database]).to eq('mongoid_test')
94
+ end
95
+
96
+ it 'does not contain any kms secret in its serialized form' do
97
+ expect(redacted.to_s).not_to include('secret-value')
98
+ expect(redacted.to_s).not_to include('AKIA')
99
+ expect(redacted.to_s).not_to include('A' * 96)
100
+ end
101
+ end
102
+
103
+ context 'when redaction would mutate the input' do
104
+ let(:config) { { password: 's3cr3t', options: { foo: 'bar' } } }
105
+
106
+ it 'does not modify the original hash' do
107
+ described_class.redact(config)
108
+ expect(config[:password]).to eq('s3cr3t')
109
+ end
110
+ end
111
+ end
112
+ end
@@ -25,5 +25,36 @@ describe Mongoid::Errors::MixedClientConfiguration do
25
25
  'Provide either only a uri as configuration'
26
26
  )
27
27
  end
28
+
29
+ context 'when the config contains sensitive values' do
30
+ let(:error) do
31
+ described_class.new(
32
+ :testing,
33
+ {
34
+ uri: 'mongodb://admin:s3cr3t@cluster.example.com/db',
35
+ password: 'standalone-secret',
36
+ options: {
37
+ auto_encryption_options: {
38
+ kms_providers: { local: { key: 'A' * 96 } }
39
+ }
40
+ }
41
+ }
42
+ )
43
+ end
44
+
45
+ it 'redacts the URI userinfo' do
46
+ expect(error.message).not_to include('admin:s3cr3t')
47
+ expect(error.message).to include('mongodb://[REDACTED]@cluster.example.com/db')
48
+ end
49
+
50
+ it 'redacts the standalone password value' do
51
+ expect(error.message).not_to include('standalone-secret')
52
+ end
53
+
54
+ it 'redacts auto_encryption_options entirely' do
55
+ expect(error.message).not_to include('kms_providers')
56
+ expect(error.message).not_to include('A' * 96)
57
+ end
58
+ end
28
59
  end
29
60
  end
@@ -25,5 +25,34 @@ describe Mongoid::Errors::NoClientDatabase do
25
25
  'If configuring via a mongoid.yml, ensure that within your :analytics'
26
26
  )
27
27
  end
28
+
29
+ context 'when the config contains sensitive values' do
30
+ let(:error) do
31
+ described_class.new(
32
+ :analytics,
33
+ {
34
+ hosts: [ '127.0.0.1:27017' ],
35
+ password: 's3cr3t',
36
+ options: {
37
+ auto_encryption_options: {
38
+ kms_providers: {
39
+ aws: { access_key_id: 'AKIAEXAMPLE', secret_access_key: 'aws-secret' }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ )
45
+ end
46
+
47
+ it 'redacts the password value' do
48
+ expect(error.message).not_to include('s3cr3t')
49
+ end
50
+
51
+ it 'redacts auto_encryption_options entirely' do
52
+ expect(error.message).not_to include('kms_providers')
53
+ expect(error.message).not_to include('aws-secret')
54
+ expect(error.message).not_to include('AKIAEXAMPLE')
55
+ end
56
+ end
28
57
  end
29
58
  end
@@ -25,5 +25,31 @@ describe Mongoid::Errors::NoClientHosts do
25
25
  'If configuring via a mongoid.yml, ensure that within your :analytics'
26
26
  )
27
27
  end
28
+
29
+ context 'when the config contains sensitive values' do
30
+ let(:error) do
31
+ described_class.new(
32
+ :analytics,
33
+ {
34
+ database: 'mongoid_test',
35
+ password: 's3cr3t',
36
+ options: {
37
+ auto_encryption_options: {
38
+ kms_providers: { local: { key: 'A' * 96 } }
39
+ }
40
+ }
41
+ }
42
+ )
43
+ end
44
+
45
+ it 'redacts the password value' do
46
+ expect(error.message).not_to include('s3cr3t')
47
+ end
48
+
49
+ it 'redacts auto_encryption_options entirely' do
50
+ expect(error.message).not_to include('kms_providers')
51
+ expect(error.message).not_to include('A' * 96)
52
+ end
53
+ end
28
54
  end
29
55
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Mongoid::Errors::ReadonlyAssociation do
6
+ let(:owner_class) do
7
+ Class.new do
8
+ def self.to_s
9
+ 'SomeOwner'
10
+ end
11
+ end
12
+ end
13
+
14
+ let(:association) do
15
+ double(name: :patients, options: { through: :appointments })
16
+ end
17
+
18
+ describe '#message' do
19
+ it 'names the association' do
20
+ error = described_class.new(owner_class, association)
21
+ expect(error.message).to include(':patients')
22
+ end
23
+
24
+ it 'names the owner class' do
25
+ error = described_class.new(owner_class, association)
26
+ expect(error.message).to include('SomeOwner')
27
+ end
28
+
29
+ it 'names the intermediate association' do
30
+ error = described_class.new(owner_class, association)
31
+ expect(error.message).to include(':appointments')
32
+ end
33
+ end
34
+ end