mongoid 9.0.8 → 9.0.10
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/mongoid/association/embedded/embeds_many/proxy.rb +3 -29
- data/lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb +0 -6
- data/lib/mongoid/association/referenced/has_many/enumerable.rb +40 -0
- data/lib/mongoid/association/referenced/has_many/proxy.rb +17 -1
- data/lib/mongoid/attributes.rb +21 -2
- data/lib/mongoid/clients/factory.rb +4 -0
- data/lib/mongoid/contextual/mongo.rb +8 -89
- data/lib/mongoid/criteria.rb +39 -5
- data/lib/mongoid/pluckable.rb +132 -0
- data/lib/mongoid/traversable.rb +0 -2
- data/lib/mongoid/version.rb +1 -1
- data/spec/mongoid/association/referenced/has_many/enumerable_spec.rb +394 -0
- data/spec/mongoid/attributes_spec.rb +30 -1
- data/spec/mongoid/clients/factory_spec.rb +32 -0
- data/spec/mongoid/criteria_spec.rb +196 -0
- data/spec/shared/CANDIDATE.md +28 -0
- data/spec/shared/lib/mrss/spec_organizer.rb +32 -3
- data/spec/shared/shlib/server.sh +1 -1
- data/spec/support/models/company.rb +2 -0
- data/spec/support/models/passport.rb +1 -0
- data/spec/support/models/product.rb +2 -0
- data/spec/support/models/seo.rb +2 -0
- metadata +7 -4
|
@@ -1819,6 +1819,400 @@ describe Mongoid::Association::Referenced::HasMany::Enumerable do
|
|
|
1819
1819
|
end
|
|
1820
1820
|
end
|
|
1821
1821
|
|
|
1822
|
+
describe '#pluck' do
|
|
1823
|
+
let(:person) do
|
|
1824
|
+
Person.create!
|
|
1825
|
+
end
|
|
1826
|
+
|
|
1827
|
+
let!(:post) do
|
|
1828
|
+
Post.create!(person_id: person.id, title: 'Test Title')
|
|
1829
|
+
end
|
|
1830
|
+
|
|
1831
|
+
let(:base) { Person }
|
|
1832
|
+
let(:association) { Person.relations[:posts] }
|
|
1833
|
+
|
|
1834
|
+
let(:criteria) do
|
|
1835
|
+
Post.where(person_id: person.id)
|
|
1836
|
+
end
|
|
1837
|
+
|
|
1838
|
+
context 'when the enumerable is not loaded' do
|
|
1839
|
+
let!(:enumerable) do
|
|
1840
|
+
described_class.new(criteria, base, association)
|
|
1841
|
+
end
|
|
1842
|
+
|
|
1843
|
+
context 'when the criteria is present' do
|
|
1844
|
+
it 'delegates to the criteria pluck method' do
|
|
1845
|
+
result = enumerable.pluck(:title)
|
|
1846
|
+
expect(result).to eq(['Test Title'])
|
|
1847
|
+
end
|
|
1848
|
+
|
|
1849
|
+
context 'when added docs are present' do
|
|
1850
|
+
it 'combines the results from the criteria and the added docs' do
|
|
1851
|
+
added_post = Post.new(title: 'Added Title', person_id: person.id)
|
|
1852
|
+
enumerable << added_post
|
|
1853
|
+
|
|
1854
|
+
expect(criteria).to receive(:pluck).with(:title).and_return(['Test Title'])
|
|
1855
|
+
result = enumerable.pluck(:title)
|
|
1856
|
+
expect(result).to eq(['Test Title', 'Added Title'])
|
|
1857
|
+
end
|
|
1858
|
+
end
|
|
1859
|
+
end
|
|
1860
|
+
|
|
1861
|
+
context 'when the criteria is not present' do
|
|
1862
|
+
let(:enumerable) { described_class.new([], base, association) }
|
|
1863
|
+
|
|
1864
|
+
it 'returns nothing' do
|
|
1865
|
+
result = enumerable.pluck(:title)
|
|
1866
|
+
expect(result).to eq([])
|
|
1867
|
+
end
|
|
1868
|
+
|
|
1869
|
+
context 'when added docs are present' do
|
|
1870
|
+
it 'returns the values from the added docs' do
|
|
1871
|
+
added_post = Post.new(title: 'Added Title', person_id: person.id)
|
|
1872
|
+
enumerable << added_post
|
|
1873
|
+
|
|
1874
|
+
result = enumerable.pluck(:title)
|
|
1875
|
+
expect(result).to eq(['Added Title'])
|
|
1876
|
+
end
|
|
1877
|
+
end
|
|
1878
|
+
end
|
|
1879
|
+
end
|
|
1880
|
+
|
|
1881
|
+
context 'when the enumerable is loaded' do
|
|
1882
|
+
let(:enumerable) { described_class.new([post], base, association) }
|
|
1883
|
+
|
|
1884
|
+
it 'returns the values from the loaded documents' do
|
|
1885
|
+
result = enumerable.pluck(:title)
|
|
1886
|
+
expect(result).to eq(['Test Title'])
|
|
1887
|
+
end
|
|
1888
|
+
|
|
1889
|
+
context 'when added docs are present' do
|
|
1890
|
+
it 'returns the values from both loaded and added docs' do
|
|
1891
|
+
added_post = Post.new(title: 'Added Title', person_id: person.id)
|
|
1892
|
+
enumerable << added_post
|
|
1893
|
+
|
|
1894
|
+
result = enumerable.pluck(:title)
|
|
1895
|
+
expect(result).to eq(['Test Title', 'Added Title'])
|
|
1896
|
+
end
|
|
1897
|
+
end
|
|
1898
|
+
end
|
|
1899
|
+
end
|
|
1900
|
+
|
|
1901
|
+
describe '#pluck with aliases' do
|
|
1902
|
+
let!(:parent) do
|
|
1903
|
+
Company.create!
|
|
1904
|
+
end
|
|
1905
|
+
|
|
1906
|
+
context 'when the field is aliased' do
|
|
1907
|
+
let!(:expensive) do
|
|
1908
|
+
parent.products.create!(price: 100000)
|
|
1909
|
+
end
|
|
1910
|
+
|
|
1911
|
+
let!(:cheap) do
|
|
1912
|
+
parent.products.create!(price: 1)
|
|
1913
|
+
end
|
|
1914
|
+
|
|
1915
|
+
context 'when using alias_attribute' do
|
|
1916
|
+
|
|
1917
|
+
let(:plucked) do
|
|
1918
|
+
parent.products.pluck(:price)
|
|
1919
|
+
end
|
|
1920
|
+
|
|
1921
|
+
it 'uses the aliases' do
|
|
1922
|
+
expect(plucked).to eq([ 100000, 1 ])
|
|
1923
|
+
end
|
|
1924
|
+
end
|
|
1925
|
+
end
|
|
1926
|
+
|
|
1927
|
+
context 'when plucking a localized field' do
|
|
1928
|
+
with_default_i18n_configs
|
|
1929
|
+
|
|
1930
|
+
before do
|
|
1931
|
+
I18n.locale = :en
|
|
1932
|
+
p = parent.products.create!(name: 'english-text')
|
|
1933
|
+
I18n.locale = :de
|
|
1934
|
+
p.name = 'deutsch-text'
|
|
1935
|
+
p.save!
|
|
1936
|
+
end
|
|
1937
|
+
|
|
1938
|
+
context 'when plucking the entire field' do
|
|
1939
|
+
let(:plucked) do
|
|
1940
|
+
parent.products.all.pluck(:name)
|
|
1941
|
+
end
|
|
1942
|
+
|
|
1943
|
+
let(:plucked_translations) do
|
|
1944
|
+
parent.products.all.pluck(:name_translations)
|
|
1945
|
+
end
|
|
1946
|
+
|
|
1947
|
+
let(:plucked_translations_both) do
|
|
1948
|
+
parent.products.all.pluck(:name_translations, :name)
|
|
1949
|
+
end
|
|
1950
|
+
|
|
1951
|
+
it 'returns the demongoized translations' do
|
|
1952
|
+
expect(plucked.first).to eq('deutsch-text')
|
|
1953
|
+
end
|
|
1954
|
+
|
|
1955
|
+
it 'returns the full translations hash to _translations' do
|
|
1956
|
+
expect(plucked_translations.first).to eq({'de'=>'deutsch-text', 'en'=>'english-text'})
|
|
1957
|
+
end
|
|
1958
|
+
|
|
1959
|
+
it 'returns both' do
|
|
1960
|
+
expect(plucked_translations_both.first).to eq([{'de'=>'deutsch-text', 'en'=>'english-text'}, 'deutsch-text'])
|
|
1961
|
+
end
|
|
1962
|
+
end
|
|
1963
|
+
|
|
1964
|
+
context 'when plucking a specific locale' do
|
|
1965
|
+
|
|
1966
|
+
let(:plucked) do
|
|
1967
|
+
parent.products.all.pluck(:'name.de')
|
|
1968
|
+
end
|
|
1969
|
+
|
|
1970
|
+
it 'returns the specific translations' do
|
|
1971
|
+
expect(plucked.first).to eq('deutsch-text')
|
|
1972
|
+
end
|
|
1973
|
+
end
|
|
1974
|
+
|
|
1975
|
+
context 'when plucking a specific locale from _translations field' do
|
|
1976
|
+
|
|
1977
|
+
let(:plucked) do
|
|
1978
|
+
parent.products.all.pluck(:'name_translations.de')
|
|
1979
|
+
end
|
|
1980
|
+
|
|
1981
|
+
it 'returns the specific translations' do
|
|
1982
|
+
expect(plucked.first).to eq('deutsch-text')
|
|
1983
|
+
end
|
|
1984
|
+
end
|
|
1985
|
+
|
|
1986
|
+
context 'when fallbacks are enabled with a locale list' do
|
|
1987
|
+
require_fallbacks
|
|
1988
|
+
|
|
1989
|
+
before do
|
|
1990
|
+
I18n.fallbacks[:he] = [ :en ]
|
|
1991
|
+
end
|
|
1992
|
+
|
|
1993
|
+
let(:plucked) do
|
|
1994
|
+
parent.products.all.pluck(:name).first
|
|
1995
|
+
end
|
|
1996
|
+
|
|
1997
|
+
it 'correctly uses the fallback' do
|
|
1998
|
+
I18n.locale = :en
|
|
1999
|
+
parent.products.create!(name: 'english-text')
|
|
2000
|
+
I18n.locale = :he
|
|
2001
|
+
expect(plucked).to eq 'english-text'
|
|
2002
|
+
end
|
|
2003
|
+
end
|
|
2004
|
+
|
|
2005
|
+
context 'when the localized field is aliased' do
|
|
2006
|
+
before do
|
|
2007
|
+
I18n.locale = :en
|
|
2008
|
+
parent.products.delete_all
|
|
2009
|
+
p = parent.products.create!(name: 'ACME Rocket Skates', tagline: 'english-text')
|
|
2010
|
+
I18n.locale = :de
|
|
2011
|
+
p.tagline = 'deutsch-text'
|
|
2012
|
+
p.save!
|
|
2013
|
+
end
|
|
2014
|
+
|
|
2015
|
+
context 'when plucking the entire field' do
|
|
2016
|
+
let(:plucked) do
|
|
2017
|
+
parent.products.all.pluck(:tagline)
|
|
2018
|
+
end
|
|
2019
|
+
|
|
2020
|
+
let(:plucked_unaliased) do
|
|
2021
|
+
parent.products.all.pluck(:tl)
|
|
2022
|
+
end
|
|
2023
|
+
|
|
2024
|
+
let(:plucked_translations) do
|
|
2025
|
+
parent.products.all.pluck(:tagline_translations)
|
|
2026
|
+
end
|
|
2027
|
+
|
|
2028
|
+
let(:plucked_translations_both) do
|
|
2029
|
+
parent.products.all.pluck(:tagline_translations, :tagline)
|
|
2030
|
+
end
|
|
2031
|
+
|
|
2032
|
+
it 'returns the demongoized translations' do
|
|
2033
|
+
expect(plucked.first).to eq('deutsch-text')
|
|
2034
|
+
end
|
|
2035
|
+
|
|
2036
|
+
it 'returns the demongoized translations when unaliased' do
|
|
2037
|
+
expect(plucked_unaliased.first).to eq('deutsch-text')
|
|
2038
|
+
end
|
|
2039
|
+
|
|
2040
|
+
it 'returns the full translations hash to _translations' do
|
|
2041
|
+
expect(plucked_translations.first).to eq({ 'de' => 'deutsch-text', 'en' => 'english-text' })
|
|
2042
|
+
end
|
|
2043
|
+
|
|
2044
|
+
it 'returns both' do
|
|
2045
|
+
expect(plucked_translations_both.first).to eq([{ 'de' => 'deutsch-text', 'en' => 'english-text' }, 'deutsch-text'])
|
|
2046
|
+
end
|
|
2047
|
+
end
|
|
2048
|
+
|
|
2049
|
+
context 'when plucking a specific locale' do
|
|
2050
|
+
|
|
2051
|
+
let(:plucked) do
|
|
2052
|
+
parent.products.all.pluck(:'tagline.de')
|
|
2053
|
+
end
|
|
2054
|
+
|
|
2055
|
+
it 'returns the specific translations' do
|
|
2056
|
+
expect(plucked.first).to eq('deutsch-text')
|
|
2057
|
+
end
|
|
2058
|
+
end
|
|
2059
|
+
|
|
2060
|
+
context 'when plucking a specific locale from _translations field' do
|
|
2061
|
+
|
|
2062
|
+
let(:plucked) do
|
|
2063
|
+
parent.products.all.pluck(:'tagline_translations.de')
|
|
2064
|
+
end
|
|
2065
|
+
|
|
2066
|
+
it 'returns the specific translations' do
|
|
2067
|
+
expect(plucked.first).to eq('deutsch-text')
|
|
2068
|
+
end
|
|
2069
|
+
end
|
|
2070
|
+
|
|
2071
|
+
context 'when fallbacks are enabled with a locale list' do
|
|
2072
|
+
require_fallbacks
|
|
2073
|
+
|
|
2074
|
+
before do
|
|
2075
|
+
I18n.fallbacks[:he] = [:en]
|
|
2076
|
+
end
|
|
2077
|
+
|
|
2078
|
+
let(:plucked) do
|
|
2079
|
+
parent.products.all.pluck(:tagline).first
|
|
2080
|
+
end
|
|
2081
|
+
|
|
2082
|
+
it 'correctly uses the fallback' do
|
|
2083
|
+
I18n.locale = :en
|
|
2084
|
+
parent.products.create!(tagline: 'english-text')
|
|
2085
|
+
I18n.locale = :he
|
|
2086
|
+
expect(plucked).to eq 'english-text'
|
|
2087
|
+
end
|
|
2088
|
+
end
|
|
2089
|
+
end
|
|
2090
|
+
|
|
2091
|
+
context 'when the localized field is embedded' do
|
|
2092
|
+
with_default_i18n_configs
|
|
2093
|
+
|
|
2094
|
+
before do
|
|
2095
|
+
s = Seo.new
|
|
2096
|
+
I18n.locale = :en
|
|
2097
|
+
s.name = 'english-text'
|
|
2098
|
+
I18n.locale = :de
|
|
2099
|
+
s.name = 'deutsch-text'
|
|
2100
|
+
|
|
2101
|
+
parent.products.delete_all
|
|
2102
|
+
parent.products.create!(name: 'ACME Tunnel Paint', seo: s)
|
|
2103
|
+
end
|
|
2104
|
+
|
|
2105
|
+
let(:plucked) do
|
|
2106
|
+
parent.products.pluck('seo.name').first
|
|
2107
|
+
end
|
|
2108
|
+
|
|
2109
|
+
let(:plucked_translations) do
|
|
2110
|
+
parent.products.pluck('seo.name_translations').first
|
|
2111
|
+
end
|
|
2112
|
+
|
|
2113
|
+
let(:plucked_translations_field) do
|
|
2114
|
+
parent.products.pluck('seo.name_translations.en').first
|
|
2115
|
+
end
|
|
2116
|
+
|
|
2117
|
+
it 'returns the translation for the current locale' do
|
|
2118
|
+
expect(plucked).to eq('deutsch-text')
|
|
2119
|
+
end
|
|
2120
|
+
|
|
2121
|
+
it 'returns the full _translation hash' do
|
|
2122
|
+
expect(plucked_translations).to eq({ 'en' => 'english-text', 'de' => 'deutsch-text' })
|
|
2123
|
+
end
|
|
2124
|
+
|
|
2125
|
+
it 'returns the translation for the requested locale' do
|
|
2126
|
+
expect(plucked_translations_field).to eq('english-text')
|
|
2127
|
+
end
|
|
2128
|
+
end
|
|
2129
|
+
end
|
|
2130
|
+
|
|
2131
|
+
context 'when the localized field is embedded and aliased' do
|
|
2132
|
+
with_default_i18n_configs
|
|
2133
|
+
|
|
2134
|
+
before do
|
|
2135
|
+
s = Seo.new
|
|
2136
|
+
I18n.locale = :en
|
|
2137
|
+
s.description = 'english-text'
|
|
2138
|
+
I18n.locale = :de
|
|
2139
|
+
s.description = 'deutsch-text'
|
|
2140
|
+
|
|
2141
|
+
parent.products.delete_all
|
|
2142
|
+
parent.products.create!(name: 'ACME Tunnel Paint', seo: s)
|
|
2143
|
+
end
|
|
2144
|
+
|
|
2145
|
+
let(:plucked) do
|
|
2146
|
+
parent.products.pluck('seo.description').first
|
|
2147
|
+
end
|
|
2148
|
+
|
|
2149
|
+
let(:plucked_unaliased) do
|
|
2150
|
+
parent.products.pluck('seo.desc').first
|
|
2151
|
+
end
|
|
2152
|
+
|
|
2153
|
+
let(:plucked_translations) do
|
|
2154
|
+
parent.products.pluck('seo.description_translations').first
|
|
2155
|
+
end
|
|
2156
|
+
|
|
2157
|
+
let(:plucked_translations_field) do
|
|
2158
|
+
parent.products.pluck('seo.description_translations.en').first
|
|
2159
|
+
end
|
|
2160
|
+
|
|
2161
|
+
it 'returns the translation for the current locale' do
|
|
2162
|
+
I18n.with_locale(:en) do
|
|
2163
|
+
expect(plucked).to eq('english-text')
|
|
2164
|
+
end
|
|
2165
|
+
end
|
|
2166
|
+
|
|
2167
|
+
it 'returns the translation for the current locale when unaliased' do
|
|
2168
|
+
I18n.with_locale(:en) do
|
|
2169
|
+
expect(plucked_unaliased).to eq('english-text')
|
|
2170
|
+
end
|
|
2171
|
+
end
|
|
2172
|
+
|
|
2173
|
+
it 'returns the full _translation hash' do
|
|
2174
|
+
expect(plucked_translations).to eq({ 'en' => 'english-text', 'de' => 'deutsch-text' })
|
|
2175
|
+
end
|
|
2176
|
+
|
|
2177
|
+
it 'returns the translation for the requested locale' do
|
|
2178
|
+
expect(plucked_translations_field).to eq('english-text')
|
|
2179
|
+
end
|
|
2180
|
+
end
|
|
2181
|
+
|
|
2182
|
+
context 'when plucking an embedded field' do
|
|
2183
|
+
let(:label) { Label.new(sales: '1E2') }
|
|
2184
|
+
let!(:band) { Band.create!(label: label) }
|
|
2185
|
+
|
|
2186
|
+
let(:plucked) { Band.where(_id: band.id).pluck('label.sales') }
|
|
2187
|
+
|
|
2188
|
+
it 'demongoizes the field' do
|
|
2189
|
+
expect(plucked).to eq([ BigDecimal('1E2') ])
|
|
2190
|
+
end
|
|
2191
|
+
end
|
|
2192
|
+
|
|
2193
|
+
context 'when plucking an embeds_many field' do
|
|
2194
|
+
let(:label) { Label.new(sales: '1E2') }
|
|
2195
|
+
let!(:band) { Band.create!(labels: [label]) }
|
|
2196
|
+
|
|
2197
|
+
let(:plucked) { Band.where(_id: band.id).pluck('labels.sales') }
|
|
2198
|
+
|
|
2199
|
+
it 'demongoizes the field' do
|
|
2200
|
+
expect(plucked.first).to eq([ BigDecimal('1E2') ])
|
|
2201
|
+
end
|
|
2202
|
+
end
|
|
2203
|
+
|
|
2204
|
+
context 'when plucking a nonexistent embedded field' do
|
|
2205
|
+
let(:label) { Label.new(sales: '1E2') }
|
|
2206
|
+
let!(:band) { Band.create!(label: label) }
|
|
2207
|
+
|
|
2208
|
+
let(:plucked) { Band.where(_id: band.id).pluck('label.qwerty') }
|
|
2209
|
+
|
|
2210
|
+
it 'returns nil' do
|
|
2211
|
+
expect(plucked.first).to eq(nil)
|
|
2212
|
+
end
|
|
2213
|
+
end
|
|
2214
|
+
end
|
|
2215
|
+
|
|
1822
2216
|
describe "#reset" do
|
|
1823
2217
|
|
|
1824
2218
|
let(:person) do
|
|
@@ -1729,6 +1729,19 @@ describe Mongoid::Attributes do
|
|
|
1729
1729
|
end
|
|
1730
1730
|
end
|
|
1731
1731
|
end
|
|
1732
|
+
|
|
1733
|
+
context 'when map_big_decimal_to_decimal128 is enabled' do
|
|
1734
|
+
config_override :map_big_decimal_to_decimal128, true
|
|
1735
|
+
|
|
1736
|
+
context 'when writing an identical number' do
|
|
1737
|
+
let(:band) { Band.create!(name: 'Nirvana', sales: 123456.78).reload }
|
|
1738
|
+
|
|
1739
|
+
it 'does not mark the document as changed' do
|
|
1740
|
+
band.sales = 123456.78
|
|
1741
|
+
expect(band.changed?).to be false
|
|
1742
|
+
end
|
|
1743
|
+
end
|
|
1744
|
+
end
|
|
1732
1745
|
end
|
|
1733
1746
|
|
|
1734
1747
|
describe "#typed_value_for" do
|
|
@@ -2709,7 +2722,23 @@ describe Mongoid::Attributes do
|
|
|
2709
2722
|
end
|
|
2710
2723
|
end
|
|
2711
2724
|
|
|
2712
|
-
context "when
|
|
2725
|
+
context "when accessing an embedded document with the attribute accessor" do
|
|
2726
|
+
let(:band) { Band.create! }
|
|
2727
|
+
|
|
2728
|
+
before do
|
|
2729
|
+
Band.where(id: band.id).update_all({
|
|
2730
|
+
:$push => {records: { _id: BSON::ObjectId.new }}
|
|
2731
|
+
})
|
|
2732
|
+
end
|
|
2733
|
+
|
|
2734
|
+
it "does not throw a conflicting update error" do
|
|
2735
|
+
b1 = Band.find(band.id)
|
|
2736
|
+
b1[:records].is_a?(Array).should be true
|
|
2737
|
+
expect { b1.save! }.not_to raise_error
|
|
2738
|
+
end
|
|
2739
|
+
end
|
|
2740
|
+
|
|
2741
|
+
context "when modifying a set referenced with the [] notation" do
|
|
2713
2742
|
let(:catalog) { Catalog.create!(set_field: [ 1 ].to_set) }
|
|
2714
2743
|
|
|
2715
2744
|
before do
|
|
@@ -30,6 +30,34 @@ describe Mongoid::Clients::Factory do
|
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
shared_examples_for 'includes rails wrapping library' do
|
|
34
|
+
context 'when Rails is available' do
|
|
35
|
+
around do |example|
|
|
36
|
+
rails_was_defined = defined?(::Rails)
|
|
37
|
+
|
|
38
|
+
if !rails_was_defined
|
|
39
|
+
module ::Rails
|
|
40
|
+
def self.version
|
|
41
|
+
'6.1.0'
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
example.run
|
|
47
|
+
|
|
48
|
+
if !rails_was_defined
|
|
49
|
+
Object.send(:remove_const, :Rails) if defined?(::Rails)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'adds Rails as another wrapping library' do
|
|
54
|
+
expect(client.options[:wrapping_libraries]).to include(
|
|
55
|
+
{'name' => 'Rails', 'version' => '6.1.0'},
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
33
61
|
describe ".create" do
|
|
34
62
|
|
|
35
63
|
context "when provided a name" do
|
|
@@ -89,6 +117,8 @@ describe Mongoid::Clients::Factory do
|
|
|
89
117
|
Mongoid::Clients::Factory::MONGOID_WRAPPING_LIBRARY)]
|
|
90
118
|
end
|
|
91
119
|
|
|
120
|
+
it_behaves_like 'includes rails wrapping library'
|
|
121
|
+
|
|
92
122
|
context 'when configuration specifies a wrapping library' do
|
|
93
123
|
|
|
94
124
|
let(:config) do
|
|
@@ -110,6 +140,8 @@ describe Mongoid::Clients::Factory do
|
|
|
110
140
|
{'name' => 'Foo'},
|
|
111
141
|
]
|
|
112
142
|
end
|
|
143
|
+
|
|
144
|
+
it_behaves_like 'includes rails wrapping library'
|
|
113
145
|
end
|
|
114
146
|
end
|
|
115
147
|
|
|
@@ -3250,5 +3250,201 @@ describe Mongoid::Criteria do
|
|
|
3250
3250
|
expect(criteria.selector).to eq({ 'name' => 'Songs Ohia' })
|
|
3251
3251
|
end
|
|
3252
3252
|
end
|
|
3253
|
+
|
|
3254
|
+
context 'with allowed methods' do
|
|
3255
|
+
context 'when using multiple query methods' do
|
|
3256
|
+
let(:hash) do
|
|
3257
|
+
{
|
|
3258
|
+
klass: Band,
|
|
3259
|
+
where: { active: true },
|
|
3260
|
+
limit: 10,
|
|
3261
|
+
skip: 5,
|
|
3262
|
+
order_by: { name: 1 }
|
|
3263
|
+
}
|
|
3264
|
+
end
|
|
3265
|
+
|
|
3266
|
+
it 'applies all methods successfully' do
|
|
3267
|
+
expect(criteria.selector).to eq({ 'active' => true })
|
|
3268
|
+
expect(criteria.options[:limit]).to eq(10)
|
|
3269
|
+
expect(criteria.options[:skip]).to eq(5)
|
|
3270
|
+
expect(criteria.options[:sort]).to eq({ 'name' => 1 })
|
|
3271
|
+
end
|
|
3272
|
+
end
|
|
3273
|
+
|
|
3274
|
+
context 'when using query selector methods' do
|
|
3275
|
+
let(:hash) do
|
|
3276
|
+
{
|
|
3277
|
+
klass: Band,
|
|
3278
|
+
gt: { members: 2 },
|
|
3279
|
+
in: { genre: ['rock', 'metal'] }
|
|
3280
|
+
}
|
|
3281
|
+
end
|
|
3282
|
+
|
|
3283
|
+
it 'applies selector methods' do
|
|
3284
|
+
expect(criteria.selector['members']).to eq({ '$gt' => 2 })
|
|
3285
|
+
expect(criteria.selector['genre']).to eq({ '$in' => ['rock', 'metal'] })
|
|
3286
|
+
end
|
|
3287
|
+
end
|
|
3288
|
+
|
|
3289
|
+
context 'when using aggregation methods' do
|
|
3290
|
+
let(:hash) do
|
|
3291
|
+
{
|
|
3292
|
+
klass: Band,
|
|
3293
|
+
project: { name: 1, members: 1 }
|
|
3294
|
+
}
|
|
3295
|
+
end
|
|
3296
|
+
|
|
3297
|
+
it 'applies aggregation methods' do
|
|
3298
|
+
expect { criteria }.not_to raise_error
|
|
3299
|
+
end
|
|
3300
|
+
end
|
|
3301
|
+
end
|
|
3302
|
+
|
|
3303
|
+
context 'with disallowed methods' do
|
|
3304
|
+
context 'when attempting to call create' do
|
|
3305
|
+
let(:hash) do
|
|
3306
|
+
{ klass: Band, create: { name: 'Malicious' } }
|
|
3307
|
+
end
|
|
3308
|
+
|
|
3309
|
+
it 'raises ArgumentError' do
|
|
3310
|
+
expect { criteria }.to raise_error(ArgumentError, "Method 'create' is not allowed in from_hash")
|
|
3311
|
+
end
|
|
3312
|
+
end
|
|
3313
|
+
|
|
3314
|
+
context 'when attempting to call create!' do
|
|
3315
|
+
let(:hash) do
|
|
3316
|
+
{ klass: Band, 'create!': { name: 'Malicious' } }
|
|
3317
|
+
end
|
|
3318
|
+
|
|
3319
|
+
it 'raises ArgumentError' do
|
|
3320
|
+
expect { criteria }.to raise_error(ArgumentError, "Method 'create!' is not allowed in from_hash")
|
|
3321
|
+
end
|
|
3322
|
+
end
|
|
3323
|
+
|
|
3324
|
+
context 'when attempting to call build' do
|
|
3325
|
+
let(:hash) do
|
|
3326
|
+
{ klass: Band, build: { name: 'Malicious' } }
|
|
3327
|
+
end
|
|
3328
|
+
|
|
3329
|
+
it 'raises ArgumentError' do
|
|
3330
|
+
expect { criteria }.to raise_error(ArgumentError, "Method 'build' is not allowed in from_hash")
|
|
3331
|
+
end
|
|
3332
|
+
end
|
|
3333
|
+
|
|
3334
|
+
context 'when attempting to call find' do
|
|
3335
|
+
let(:hash) do
|
|
3336
|
+
{ klass: Band, find: 'some_id' }
|
|
3337
|
+
end
|
|
3338
|
+
|
|
3339
|
+
it 'raises ArgumentError' do
|
|
3340
|
+
expect { criteria }.to raise_error(ArgumentError, "Method 'find' is not allowed in from_hash")
|
|
3341
|
+
end
|
|
3342
|
+
end
|
|
3343
|
+
|
|
3344
|
+
context 'when attempting to call execute_or_raise' do
|
|
3345
|
+
let(:hash) do
|
|
3346
|
+
{ klass: Band, execute_or_raise: ['id1', 'id2'] }
|
|
3347
|
+
end
|
|
3348
|
+
|
|
3349
|
+
it 'raises ArgumentError' do
|
|
3350
|
+
expect { criteria }.to raise_error(ArgumentError, "Method 'execute_or_raise' is not allowed in from_hash")
|
|
3351
|
+
end
|
|
3352
|
+
end
|
|
3353
|
+
|
|
3354
|
+
context 'when attempting to call new' do
|
|
3355
|
+
let(:hash) do
|
|
3356
|
+
{ klass: Band, new: { name: 'Test' } }
|
|
3357
|
+
end
|
|
3358
|
+
|
|
3359
|
+
it 'raises ArgumentError' do
|
|
3360
|
+
expect { criteria }.to raise_error(ArgumentError, "Method 'new' is not allowed in from_hash")
|
|
3361
|
+
end
|
|
3362
|
+
end
|
|
3363
|
+
|
|
3364
|
+
context 'when allowed method is combined with disallowed method' do
|
|
3365
|
+
let(:hash) do
|
|
3366
|
+
{
|
|
3367
|
+
klass: Band,
|
|
3368
|
+
where: { active: true },
|
|
3369
|
+
create: { name: 'Malicious' }
|
|
3370
|
+
}
|
|
3371
|
+
end
|
|
3372
|
+
|
|
3373
|
+
it 'raises ArgumentError before executing any methods' do
|
|
3374
|
+
expect { criteria }.to raise_error(ArgumentError, "Method 'create' is not allowed in from_hash")
|
|
3375
|
+
end
|
|
3376
|
+
end
|
|
3377
|
+
end
|
|
3378
|
+
|
|
3379
|
+
context 'security validation' do
|
|
3380
|
+
# This test ensures that ALL public methods not in the allowlist are blocked
|
|
3381
|
+
it 'blocks all dangerous public methods' do
|
|
3382
|
+
dangerous_methods = %i[
|
|
3383
|
+
build create create! new
|
|
3384
|
+
find find_or_create_by find_or_create_by! find_or_initialize_by
|
|
3385
|
+
first_or_create first_or_create! first_or_initialize
|
|
3386
|
+
execute_or_raise multiple_from_db for_ids
|
|
3387
|
+
documents= inclusions= scoping_options=
|
|
3388
|
+
initialize freeze as_json
|
|
3389
|
+
]
|
|
3390
|
+
|
|
3391
|
+
dangerous_methods.each do |method|
|
|
3392
|
+
hash = { klass: Band, method => 'arg' }
|
|
3393
|
+
expect { described_class.from_hash(hash) }.to raise_error(
|
|
3394
|
+
ArgumentError,
|
|
3395
|
+
"Method '#{method}' is not allowed in from_hash"
|
|
3396
|
+
), "Expected method '#{method}' to be blocked but it was allowed"
|
|
3397
|
+
end
|
|
3398
|
+
end
|
|
3399
|
+
|
|
3400
|
+
it 'blocks dangerous inherited methods from Object' do
|
|
3401
|
+
# Critical security test: block send, instance_eval, etc.
|
|
3402
|
+
inherited_dangerous = %i[
|
|
3403
|
+
send __send__ instance_eval instance_exec
|
|
3404
|
+
instance_variable_set method
|
|
3405
|
+
]
|
|
3406
|
+
|
|
3407
|
+
inherited_dangerous.each do |method|
|
|
3408
|
+
hash = { klass: Band, method => 'arg' }
|
|
3409
|
+
expect { described_class.from_hash(hash) }.to raise_error(
|
|
3410
|
+
ArgumentError,
|
|
3411
|
+
"Method '#{method}' is not allowed in from_hash"
|
|
3412
|
+
), "Expected inherited method '#{method}' to be blocked"
|
|
3413
|
+
end
|
|
3414
|
+
end
|
|
3415
|
+
|
|
3416
|
+
it 'blocks Enumerable execution methods' do
|
|
3417
|
+
# from_hash should build queries, not execute them
|
|
3418
|
+
enumerable_methods = %i[each map select count sum]
|
|
3419
|
+
|
|
3420
|
+
enumerable_methods.each do |method|
|
|
3421
|
+
hash = { klass: Band, method => 'arg' }
|
|
3422
|
+
expect { described_class.from_hash(hash) }.to raise_error(
|
|
3423
|
+
ArgumentError,
|
|
3424
|
+
"Method '#{method}' is not allowed in from_hash"
|
|
3425
|
+
), "Expected Enumerable method '#{method}' to be blocked"
|
|
3426
|
+
end
|
|
3427
|
+
end
|
|
3428
|
+
|
|
3429
|
+
it 'allows all whitelisted methods' do
|
|
3430
|
+
# Sample of allowed methods from each category
|
|
3431
|
+
allowed_sample = {
|
|
3432
|
+
where: { name: 'Test' }, # Query selector
|
|
3433
|
+
limit: 10, # Query option
|
|
3434
|
+
skip: 5, # Query option
|
|
3435
|
+
gt: { age: 18 }, # Query selector
|
|
3436
|
+
in: { status: ['active'] }, # Query selector
|
|
3437
|
+
ascending: :name, # Sorting
|
|
3438
|
+
includes: :notes, # Eager loading
|
|
3439
|
+
merge: { klass: Band }, # Merge
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
allowed_sample.each do |method, args|
|
|
3443
|
+
hash = { klass: Band, method => args }
|
|
3444
|
+
expect { described_class.from_hash(hash) }.not_to raise_error,
|
|
3445
|
+
"Expected method '#{method}' to be allowed but it was blocked"
|
|
3446
|
+
end
|
|
3447
|
+
end
|
|
3448
|
+
end
|
|
3253
3449
|
end
|
|
3254
3450
|
end
|