mongoid 9.0.10 → 9.0.12

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -3
  3. data/Rakefile +31 -0
  4. data/lib/config/locales/en.yml +45 -0
  5. data/lib/mongoid/association/depending.rb +8 -4
  6. data/lib/mongoid/association/nested/many.rb +38 -3
  7. data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
  8. data/lib/mongoid/association/referenced/auto_save.rb +48 -6
  9. data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
  10. data/lib/mongoid/association/relatable.rb +17 -0
  11. data/lib/mongoid/collection_configurable.rb +8 -0
  12. data/lib/mongoid/config/defaults.rb +9 -5
  13. data/lib/mongoid/config/encryption.rb +36 -18
  14. data/lib/mongoid/config.rb +72 -0
  15. data/lib/mongoid/contextual/aggregable/memory.rb +5 -2
  16. data/lib/mongoid/contextual/memory.rb +18 -7
  17. data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
  18. data/lib/mongoid/criteria/queryable/selectable.rb +109 -0
  19. data/lib/mongoid/encryptable.rb +46 -0
  20. data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
  21. data/lib/mongoid/errors/no_encryption_schema.rb +28 -0
  22. data/lib/mongoid/errors.rb +2 -0
  23. data/lib/mongoid/field_readable.rb +70 -0
  24. data/lib/mongoid/indexable.rb +39 -27
  25. data/lib/mongoid/matchable.rb +6 -1
  26. data/lib/mongoid/matcher/eq_impl_with_regexp.rb +4 -9
  27. data/lib/mongoid/matcher/regex.rb +9 -13
  28. data/lib/mongoid/matcher/regexp_budget.rb +383 -0
  29. data/lib/mongoid/matcher.rb +1 -0
  30. data/lib/mongoid/persistence_context.rb +35 -0
  31. data/lib/mongoid/search_indexable.rb +14 -7
  32. data/lib/mongoid/tasks/database.rb +23 -9
  33. data/lib/mongoid/threaded.rb +37 -0
  34. data/lib/mongoid/version.rb +1 -1
  35. data/lib/mongoid/warnings.rb +4 -0
  36. data/spec/integration/app_spec.rb +201 -36
  37. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
  38. data/spec/integration/dots_and_dollars_spec.rb +12 -2
  39. data/spec/integration/encryption_spec.rb +149 -0
  40. data/spec/integration/matcher_operator_data/regex.yml +21 -0
  41. data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
  42. data/spec/integration/query_operator_guard_spec.rb +89 -0
  43. data/spec/mongoid/association/auto_save_spec.rb +98 -0
  44. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
  45. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
  46. data/spec/mongoid/attributes/nested_spec.rb +250 -30
  47. data/spec/mongoid/config/defaults_spec.rb +18 -0
  48. data/spec/mongoid/config/encryption_spec.rb +228 -0
  49. data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
  50. data/spec/mongoid/contextual/memory_spec.rb +177 -0
  51. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +2 -0
  52. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +200 -0
  53. data/spec/mongoid/criteria_spec.rb +2 -0
  54. data/spec/mongoid/encryptable_spec.rb +61 -0
  55. data/spec/mongoid/errors/document_not_found_spec.rb +10 -3
  56. data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
  57. data/spec/mongoid/tasks/database_spec.rb +18 -0
  58. data/spec/support/crypt/models.rb +157 -0
  59. metadata +28 -2
@@ -582,4 +582,95 @@ describe Mongoid::Contextual::Aggregable::Memory do
582
582
  end
583
583
  end
584
584
  end
585
+
586
+ context 'when the field name is a method name' do
587
+ let!(:depeche) do
588
+ Band.create!(name: 'Depeche Mode', likes: 1000)
589
+ end
590
+
591
+ let(:criteria) do
592
+ Band.all.tap do |crit|
593
+ crit.documents = [ depeche ]
594
+ end
595
+ end
596
+
597
+ it 'does not call the method for sum' do
598
+ expect(depeche).not_to receive(:delete)
599
+ expect(context.sum(:delete)).to eq(0)
600
+ end
601
+
602
+ it 'does not call the method for min' do
603
+ expect(depeche).not_to receive(:delete)
604
+ expect(context.min(:delete)).to be_nil
605
+ end
606
+
607
+ it 'does not call the method for max' do
608
+ expect(depeche).not_to receive(:delete)
609
+ expect(context.max(:delete)).to be_nil
610
+ end
611
+
612
+ it 'does not call the method for avg' do
613
+ expect(depeche).not_to receive(:delete)
614
+ expect(context.avg(:delete)).to be_nil
615
+ end
616
+
617
+ it 'does not call the method for aggregates' do
618
+ expect(depeche).not_to receive(:delete)
619
+ expect(context.aggregates(:delete))
620
+ .to eq('count' => 0, 'avg' => nil, 'max' => nil, 'min' => nil, 'sum' => 0)
621
+ end
622
+
623
+ it 'does not call a private method for avg' do
624
+ expect(depeche).not_to receive(:exit)
625
+ expect(context.avg(:exit)).to be_nil
626
+ end
627
+ end
628
+
629
+ context 'when the field is not defined' do
630
+ let!(:depeche) do
631
+ Band.create!(name: 'Depeche Mode', likes: 1000)
632
+ end
633
+
634
+ let(:criteria) do
635
+ Band.all.tap do |crit|
636
+ crit.documents = [ depeche ]
637
+ end
638
+ end
639
+
640
+ it 'returns zero for sum' do
641
+ expect(context.sum(:not_a_field)).to eq(0)
642
+ end
643
+
644
+ it 'returns nil for min' do
645
+ expect(context.min(:not_a_field)).to be_nil
646
+ end
647
+
648
+ it 'returns nil for max' do
649
+ expect(context.max(:not_a_field)).to be_nil
650
+ end
651
+
652
+ it 'returns nil for avg' do
653
+ expect(context.avg(:not_a_field)).to be_nil
654
+ end
655
+ end
656
+
657
+ context 'when the field is the foreign key of a many to many association' do
658
+ let!(:preference) do
659
+ Preference.create!(name: 'nature')
660
+ end
661
+
662
+ let!(:person) do
663
+ Person.new(preferences: [ preference ])
664
+ end
665
+
666
+ let(:criteria) do
667
+ Person.all.tap do |crit|
668
+ crit.documents = [ person ]
669
+ end
670
+ end
671
+
672
+ it 'aggregates the stored ids' do
673
+ expect(context.max(:preference_ids)).to eq([ preference.id ])
674
+ end
675
+ end
585
676
  end
@@ -588,6 +588,27 @@ describe Mongoid::Contextual::Memory do
588
588
  expect(context.distinct("label.sales")).to eq([ BigDecimal("1E2") ])
589
589
  end
590
590
  end
591
+
592
+ context 'when the field name is a method name' do
593
+ let!(:person) do
594
+ Person.create!(ssn: 'secret-ssn')
595
+ end
596
+
597
+ let!(:address) do
598
+ person.addresses.create!(street: 'hobrecht')
599
+ end
600
+
601
+ let(:criteria) do
602
+ Address.all.tap do |crit|
603
+ crit.documents = [ address ]
604
+ end
605
+ end
606
+
607
+ it 'does not call the method' do
608
+ expect(address).not_to receive(:destroy)
609
+ expect(context.distinct(:destroy)).to eq([ nil ])
610
+ end
611
+ end
591
612
  end
592
613
 
593
614
  describe "#each" do
@@ -1826,6 +1847,116 @@ describe Mongoid::Contextual::Memory do
1826
1847
  ])
1827
1848
  end
1828
1849
  end
1850
+
1851
+ context 'when plucking a dynamic attribute' do
1852
+ let(:criteria) do
1853
+ Band.all.tap do |crit|
1854
+ crit.documents = [ Band.create!(name: 'Depeche Mode', mood: 'dark') ]
1855
+ end
1856
+ end
1857
+
1858
+ it 'returns the value from the attributes' do
1859
+ expect(context.pluck(:mood)).to eq([ 'dark' ])
1860
+ end
1861
+ end
1862
+
1863
+ context 'when the field name is a method name' do
1864
+ let!(:person) do
1865
+ Person.create!(ssn: 'secret-ssn')
1866
+ end
1867
+
1868
+ let!(:address) do
1869
+ person.addresses.create!(street: 'hobrecht')
1870
+ end
1871
+
1872
+ let(:criteria) do
1873
+ Address.all.tap do |crit|
1874
+ crit.documents = [ address ]
1875
+ end
1876
+ end
1877
+
1878
+ it 'does not call the method' do
1879
+ expect(address).not_to receive(:destroy)
1880
+ expect(context.pluck(:destroy)).to eq([ nil ])
1881
+ end
1882
+
1883
+ it 'does not return the attributes hash' do
1884
+ expect(context.pluck(:attributes)).to eq([ nil ])
1885
+ end
1886
+
1887
+ it 'does not traverse to the parent document' do
1888
+ expect(context.pluck('_root.ssn')).to eq([ nil ])
1889
+ end
1890
+
1891
+ it 'does not destroy the parent document' do
1892
+ expect(context.pluck('_root.destroy')).to eq([ nil ])
1893
+ expect(Person.where(_id: person.id).count).to eq(1)
1894
+ end
1895
+ end
1896
+
1897
+ context 'when plucking a belongs_to association' do
1898
+ let!(:band) do
1899
+ Band.create!(name: 'Depeche Mode')
1900
+ end
1901
+
1902
+ let!(:artist) do
1903
+ Artist.create!(band: band)
1904
+ end
1905
+
1906
+ let(:criteria) do
1907
+ Artist.all.tap do |crit|
1908
+ crit.documents = [ artist ]
1909
+ end
1910
+ end
1911
+
1912
+ it 'traverses to the associated document' do
1913
+ expect(context.pluck('band.name')).to eq([ 'Depeche Mode' ])
1914
+ end
1915
+
1916
+ it 'returns the document for the association name' do
1917
+ expect(context.pluck(:band)).to eq([ band ])
1918
+ end
1919
+
1920
+ it 'returns the id for the foreign key' do
1921
+ expect(context.pluck(:band_id)).to eq([ band.id ])
1922
+ end
1923
+ end
1924
+
1925
+ context 'when plucking the foreign key of a many to many association' do
1926
+ let!(:preference) do
1927
+ Preference.create!(name: 'nature')
1928
+ end
1929
+
1930
+ let!(:person) do
1931
+ Person.create!(preferences: [ preference ])
1932
+ end
1933
+
1934
+ let(:criteria) do
1935
+ Person.all.tap do |crit|
1936
+ crit.documents = [ person ]
1937
+ end
1938
+ end
1939
+
1940
+ it 'returns the stored ids' do
1941
+ expect(context.pluck(:preference_ids)).to eq([ [ preference.id ] ])
1942
+ end
1943
+ end
1944
+
1945
+ context 'when the field path has an empty segment' do
1946
+ let!(:band) do
1947
+ Band.create!(name: 'Depeche Mode', label: Label.new(name: 'Mute'))
1948
+ end
1949
+
1950
+ let(:criteria) do
1951
+ Band.all.tap do |crit|
1952
+ crit.documents = [ band ]
1953
+ end
1954
+ end
1955
+
1956
+ it 'returns nil' do
1957
+ expect(context.pluck('label..name')).to eq([ nil ])
1958
+ end
1959
+ end
1829
1960
  end
1830
1961
 
1831
1962
  describe "#pick" do
@@ -1886,6 +2017,27 @@ describe Mongoid::Contextual::Memory do
1886
2017
  expect(picked).to be_nil
1887
2018
  end
1888
2019
  end
2020
+
2021
+ context 'when the field name is a method name' do
2022
+ let!(:person) do
2023
+ Person.create!(ssn: 'secret-ssn')
2024
+ end
2025
+
2026
+ let!(:address) do
2027
+ person.addresses.create!(street: 'hobrecht')
2028
+ end
2029
+
2030
+ let(:criteria) do
2031
+ Address.all.tap do |crit|
2032
+ crit.documents = [ address ]
2033
+ end
2034
+ end
2035
+
2036
+ it 'does not call the method' do
2037
+ expect(address).not_to receive(:destroy)
2038
+ expect(context.pick(:destroy)).to be_nil
2039
+ end
2040
+ end
1889
2041
  end
1890
2042
 
1891
2043
  describe "#tally" do
@@ -2340,6 +2492,31 @@ describe Mongoid::Contextual::Memory do
2340
2492
  )
2341
2493
  end
2342
2494
  end
2495
+
2496
+ context 'when the field name is a method name' do
2497
+ let!(:person) do
2498
+ Person.create!(ssn: 'secret-ssn')
2499
+ end
2500
+
2501
+ let!(:address) do
2502
+ person.addresses.create!(street: 'hobrecht')
2503
+ end
2504
+
2505
+ let(:criteria) do
2506
+ Address.all.tap do |crit|
2507
+ crit.documents = [ address ]
2508
+ end
2509
+ end
2510
+
2511
+ it 'does not call the method' do
2512
+ expect(address).not_to receive(:destroy)
2513
+ expect(context.tally(:destroy)).to eq(nil => 1)
2514
+ end
2515
+
2516
+ it 'does not disclose the parent document fields' do
2517
+ expect(context.tally('_root.ssn')).to eq(nil => 1)
2518
+ end
2519
+ end
2343
2520
  end
2344
2521
 
2345
2522
  describe '#inc' do
@@ -1830,6 +1830,8 @@ describe Mongoid::Criteria::Queryable::Selectable do
1830
1830
  end
1831
1831
 
1832
1832
  context 'when the following criteria uses string were form' do
1833
+ # String criteria compile to $where, which requires the opt-in.
1834
+ config_override :allow_unsafe_query_operators, true
1833
1835
 
1834
1836
  let(:selection) do
1835
1837
  query.not.where('hello world')
@@ -36,6 +36,8 @@ describe Mongoid::Criteria::Queryable::Selectable do
36
36
  it_behaves_like 'requires a non-nil argument'
37
37
 
38
38
  context "when provided a string" do
39
+ # String criteria compile to $where, which requires the opt-in.
40
+ config_override :allow_unsafe_query_operators, true
39
41
 
40
42
  let(:selection) do
41
43
  query.where("this.value = 10")
@@ -587,4 +589,202 @@ describe Mongoid::Criteria::Queryable::Selectable do
587
589
  end
588
590
  end
589
591
  end
592
+
593
+ describe 'top-level operator injection guard' do
594
+ context 'when allow_unsafe_query_operators is true' do
595
+ config_override :allow_unsafe_query_operators, true
596
+
597
+ context 'when passing $where' do
598
+ it 'does not raise' do
599
+ expect do
600
+ query.where('$where' => 'this.name == "admin"')
601
+ end.not_to raise_error
602
+ end
603
+ end
604
+
605
+ context 'when passing a string criterion' do
606
+ it 'does not raise' do
607
+ expect do
608
+ query.where('this.name == "admin"')
609
+ end.not_to raise_error
610
+ end
611
+ end
612
+
613
+ context 'when passing a string criterion to a negated query' do
614
+ it 'does not raise' do
615
+ expect do
616
+ query.not.where('this.name == "admin"')
617
+ end.not_to raise_error
618
+ end
619
+ end
620
+
621
+ context 'when passing $function' do
622
+ it 'does not raise' do
623
+ expect do
624
+ query.where('$function' => { body: 'function() { return true; }', args: [], lang: 'js' })
625
+ end.not_to raise_error
626
+ end
627
+ end
628
+ end
629
+
630
+ context 'when allow_unsafe_query_operators is false' do
631
+ config_override :allow_unsafe_query_operators, false
632
+
633
+ context 'when passing $where' do
634
+ it 'raises InvalidQuery' do
635
+ expect do
636
+ query.where('$where' => 'this.name == "admin"')
637
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /\$where/)
638
+ end
639
+ end
640
+
641
+ context 'when passing $function' do
642
+ it 'raises InvalidQuery' do
643
+ expect do
644
+ query.where('$function' => { body: 'function() { return true; }', args: [], lang: 'js' })
645
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /\$function/)
646
+ end
647
+ end
648
+
649
+ context 'when the error mentions allow_unsafe_query_operators' do
650
+ it 'includes the config opt-in in the message' do
651
+ expect do
652
+ query.where('$where' => 'true')
653
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /allow_unsafe_query_operators/)
654
+ end
655
+ end
656
+
657
+ context 'when passing a string criterion' do
658
+ it 'raises InvalidQuery' do
659
+ expect do
660
+ query.where('this.name == "admin"')
661
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /\$where/)
662
+ end
663
+
664
+ it 'includes the config opt-in in the message' do
665
+ expect do
666
+ query.where('this.name == "admin"')
667
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /allow_unsafe_query_operators/)
668
+ end
669
+ end
670
+
671
+ context 'when passing a string criterion to a negated query' do
672
+ it 'raises InvalidQuery' do
673
+ expect do
674
+ query.not.where('this.name == "admin"')
675
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /\$where/)
676
+ end
677
+ end
678
+
679
+ %w[$and $or $nor $not $text $comment $expr $jsonSchema $alwaysFalse $alwaysTrue].each do |op|
680
+ context "when passing #{op} (allowlisted)" do
681
+ it 'does not raise' do
682
+ value = case op
683
+ when '$and', '$or', '$nor', '$not' then [ { 'x' => 1 } ]
684
+ when '$text' then { '$search' => 'hi' }
685
+ when '$expr' then { '$gt' => [ '$a', 1 ] }
686
+ when '$jsonSchema' then { 'required' => [ 'x' ] }
687
+ else true
688
+ end
689
+ expect { query.where(op => value) }.not_to raise_error
690
+ end
691
+ end
692
+ end
693
+ end
694
+ end
695
+
696
+ describe 'nested operator injection guard' do
697
+ js = 'this.name == "admin"'
698
+
699
+ function_expr = {
700
+ '$function' => { 'body' => 'function() { return true; }', 'args' => [], 'lang' => 'js' }
701
+ }.freeze
702
+
703
+ accumulator_expr = {
704
+ '$accumulator' => {
705
+ 'init' => 'function() { return 0; }',
706
+ 'accumulate' => 'function() { return 0; }',
707
+ 'accumulateArgs' => [],
708
+ 'merge' => 'function() { return 0; }',
709
+ 'lang' => 'js'
710
+ }
711
+ }.freeze
712
+
713
+ # Every path into the selector that does not route through #expr_query,
714
+ # plus the nested forms that a top-level key check cannot see.
715
+ unsafe_queries = {
716
+ 'and with a $where key' => ->(query) { query.and('$where' => js) },
717
+ 'or with a $where key' => ->(query) { query.or('$where' => js) },
718
+ 'nor with a $where key' => ->(query) { query.nor('$where' => js) },
719
+ 'not with a $where key' => ->(query) { query.not('$where' => js) },
720
+ 'any_of with a $where key' => ->(query) { query.any_of('$where' => js) },
721
+ 'none_of with a $where key' => ->(query) { query.none_of('$where' => js) },
722
+ 'elem_match with a nested $where' => ->(query) { query.elem_match(a: { '$where' => js }) },
723
+ 'where with $where inside $or' => ->(query) { query.where('$or' => [ { '$where' => js } ]) },
724
+ 'where with $function inside $expr' => ->(query) { query.where('$expr' => function_expr) },
725
+ 'where with $accumulator inside $expr' => ->(query) { query.where('$expr' => accumulator_expr) },
726
+ 'where with $where three levels deep' => lambda { |query|
727
+ query.where('$and' => [ { '$or' => [ { '$where' => js } ] } ])
728
+ },
729
+ 'where with a symbol $where key inside $or' => ->(query) { query.where('$or' => [ { :$where => js } ]) }
730
+ }.freeze
731
+
732
+ context 'when allow_unsafe_query_operators is false' do
733
+ config_override :allow_unsafe_query_operators, false
734
+
735
+ unsafe_queries.each do |description, builder|
736
+ context "when querying with #{description}" do
737
+ it 'raises InvalidQuery' do
738
+ expect do
739
+ builder.call(query)
740
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /\$where|\$function|\$accumulator/)
741
+ end
742
+ end
743
+ end
744
+
745
+ it 'includes the config opt-in in the message' do
746
+ expect do
747
+ query.or('$where' => js)
748
+ end.to raise_error(Mongoid::Errors::InvalidQuery, /allow_unsafe_query_operators/)
749
+ end
750
+
751
+ context 'when the nested expression is safe' do
752
+ it 'permits $or with field expressions' do
753
+ expect do
754
+ query.where('$or' => [ { 'a' => 1 }, { 'b' => { '$gt' => 2 } } ])
755
+ end.not_to raise_error
756
+ end
757
+
758
+ it 'permits $expr with aggregation operators' do
759
+ expect do
760
+ query.where('$expr' => { '$gt' => [ '$a', '$b' ] })
761
+ end.not_to raise_error
762
+ end
763
+
764
+ it 'permits a field whose name resembles a javascript operator' do
765
+ expect do
766
+ query.where('where' => js)
767
+ end.not_to raise_error
768
+ end
769
+
770
+ it 'permits a value that resembles a javascript operator' do
771
+ expect do
772
+ query.where('name' => '$where')
773
+ end.not_to raise_error
774
+ end
775
+ end
776
+ end
777
+
778
+ context 'when allow_unsafe_query_operators is true' do
779
+ config_override :allow_unsafe_query_operators, true
780
+
781
+ unsafe_queries.each do |description, builder|
782
+ context "when querying with #{description}" do
783
+ it 'does not raise' do
784
+ expect { builder.call(query) }.not_to raise_error
785
+ end
786
+ end
787
+ end
788
+ end
789
+ end
590
790
  end
@@ -2432,6 +2432,8 @@ describe Mongoid::Criteria do
2432
2432
  end
2433
2433
 
2434
2434
  context "when the criteria is not embedded" do
2435
+ # String criteria compile to $where, which requires the opt-in.
2436
+ config_override :allow_unsafe_query_operators, true
2435
2437
 
2436
2438
  let(:criteria) do
2437
2439
  Band.where("this.name == 'Depeche Mode'")
@@ -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
@@ -64,9 +64,16 @@ describe Mongoid::Errors::DocumentNotFound do
64
64
  end
65
65
 
66
66
  it "contains the problem in the message" do
67
- expect(error.message).to include(
68
- "Document not found for class Person with attributes {:name=>\"syd\"}."
69
- )
67
+ # Ruby 3.4+ changed Hash#inspect format from {:name=>"syd"} to {name: "syd"}
68
+ if RUBY_VERSION >= '3.4'
69
+ expect(error.message).to include(
70
+ "Document not found for class Person with attributes {name: \"syd\"}."
71
+ )
72
+ else
73
+ expect(error.message).to include(
74
+ "Document not found for class Person with attributes {:name=>\"syd\"}."
75
+ )
76
+ end
70
77
  end
71
78
 
72
79
  it "contains the summary in the message" do