mongoid 5.0.2 → 5.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/LICENSE +1 -1
- data/README.md +1 -1
- data/lib/mongoid/changeable.rb +1 -1
- data/lib/mongoid/clients.rb +1 -0
- data/lib/mongoid/clients/options.rb +119 -7
- data/lib/mongoid/config.rb +7 -0
- data/lib/mongoid/config/options.rb +15 -0
- data/lib/mongoid/contextual/geo_near.rb +12 -0
- data/lib/mongoid/contextual/mongo.rb +6 -0
- data/lib/mongoid/criteria.rb +2 -56
- data/lib/mongoid/criteria/findable.rb +4 -1
- data/lib/mongoid/criteria/includable.rb +142 -0
- data/lib/mongoid/criteria/modifiable.rb +13 -1
- data/lib/mongoid/document.rb +21 -0
- data/lib/mongoid/fields/foreign_key.rb +5 -1
- data/lib/mongoid/fields/localized.rb +16 -1
- data/lib/mongoid/fields/validators/macro.rb +1 -0
- data/lib/mongoid/findable.rb +1 -0
- data/lib/mongoid/loggable.rb +1 -1
- data/lib/mongoid/matchable.rb +6 -1
- data/lib/mongoid/persistable.rb +2 -2
- data/lib/mongoid/relations/eager.rb +12 -5
- data/lib/mongoid/relations/eager/base.rb +3 -1
- data/lib/mongoid/relations/referenced/many.rb +39 -6
- data/lib/mongoid/relations/targets/enumerable.rb +3 -3
- data/lib/mongoid/scopable.rb +5 -2
- data/lib/mongoid/version.rb +1 -1
- data/spec/app/models/address.rb +2 -0
- data/spec/app/models/agent.rb +2 -0
- data/spec/app/models/alert.rb +2 -0
- data/spec/app/models/post.rb +1 -0
- data/spec/config/mongoid.yml +1 -0
- data/spec/mongoid/changeable_spec.rb +1 -1
- data/spec/mongoid/clients/options_spec.rb +57 -0
- data/spec/mongoid/config_spec.rb +38 -0
- data/spec/mongoid/contextual/geo_near_spec.rb +19 -0
- data/spec/mongoid/criteria/findable_spec.rb +11 -0
- data/spec/mongoid/criteria/modifiable_spec.rb +126 -0
- data/spec/mongoid/criteria_spec.rb +81 -5
- data/spec/mongoid/document_spec.rb +56 -0
- data/spec/mongoid/fields/foreign_key_spec.rb +23 -0
- data/spec/mongoid/fields/localized_spec.rb +32 -14
- data/spec/mongoid/matchable_spec.rb +127 -1
- data/spec/mongoid/persistable_spec.rb +24 -0
- data/spec/mongoid/relations/embedded/many_spec.rb +16 -0
- data/spec/mongoid/relations/referenced/many_spec.rb +60 -0
- data/spec/mongoid/relations/referenced/many_to_many_spec.rb +40 -0
- data/spec/mongoid/scopable_spec.rb +67 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/support/authorization.rb +2 -1
- metadata +6 -5
- metadata.gz.sig +0 -0
@@ -437,4 +437,23 @@ describe Mongoid::Contextual::GeoNear do
|
|
437
437
|
expect(geo_near.time).to_not be_nil
|
438
438
|
end
|
439
439
|
end
|
440
|
+
|
441
|
+
describe "#empty_and_chainable" do
|
442
|
+
|
443
|
+
let!(:collection) do
|
444
|
+
Bar.collection
|
445
|
+
end
|
446
|
+
|
447
|
+
let(:criteria) do
|
448
|
+
Bar.all
|
449
|
+
end
|
450
|
+
|
451
|
+
let(:geo_near) do
|
452
|
+
described_class.new(collection, criteria, [ 52, 13 ])
|
453
|
+
end
|
454
|
+
|
455
|
+
it "returns true" do
|
456
|
+
expect(geo_near.empty_and_chainable?).to be(true)
|
457
|
+
end
|
458
|
+
end
|
440
459
|
end
|
@@ -54,6 +54,17 @@ describe Mongoid::Criteria::Findable do
|
|
54
54
|
it "returns the matching document" do
|
55
55
|
expect(found).to eq(band)
|
56
56
|
end
|
57
|
+
|
58
|
+
context "when finding by a JSON-dumped id" do
|
59
|
+
|
60
|
+
let(:found) do
|
61
|
+
Band.find(JSON.load(JSON.dump(band.id)))
|
62
|
+
end
|
63
|
+
|
64
|
+
it "properly parses the id format" do
|
65
|
+
expect(found).to eq(band)
|
66
|
+
end
|
67
|
+
end
|
57
68
|
end
|
58
69
|
|
59
70
|
context "when the id does not match" do
|
@@ -1227,4 +1227,130 @@ describe Mongoid::Criteria::Modifiable do
|
|
1227
1227
|
end
|
1228
1228
|
end
|
1229
1229
|
end
|
1230
|
+
|
1231
|
+
describe '#create_with' do
|
1232
|
+
|
1233
|
+
context 'when called on the class' do
|
1234
|
+
|
1235
|
+
let(:attrs) do
|
1236
|
+
{ 'username' => 'Turnip' }
|
1237
|
+
end
|
1238
|
+
|
1239
|
+
it 'returns a criteria with the defined attributes' do
|
1240
|
+
expect(Person.create_with(attrs).selector).to eq(attrs)
|
1241
|
+
end
|
1242
|
+
|
1243
|
+
context 'when a method is chained' do
|
1244
|
+
|
1245
|
+
context 'when a write method is chained' do
|
1246
|
+
|
1247
|
+
it 'executes the method' do
|
1248
|
+
expect(Person.create_with(attrs).new.username).to eq('Turnip')
|
1249
|
+
end
|
1250
|
+
end
|
1251
|
+
|
1252
|
+
context 'when a write method is chained' do
|
1253
|
+
|
1254
|
+
let(:query) do
|
1255
|
+
{ 'age' => 50 }
|
1256
|
+
end
|
1257
|
+
|
1258
|
+
let(:new_person) do
|
1259
|
+
Person.create_with(attrs).find_or_create_by(query)
|
1260
|
+
end
|
1261
|
+
|
1262
|
+
it 'executes the write' do
|
1263
|
+
expect(new_person.username).to eq('Turnip')
|
1264
|
+
expect(new_person.age).to eq(50)
|
1265
|
+
end
|
1266
|
+
|
1267
|
+
context 'when the attributes are shared with the write method args' do
|
1268
|
+
|
1269
|
+
let(:query) do
|
1270
|
+
{ 'username' => 'Beet', 'age' => 50 }
|
1271
|
+
end
|
1272
|
+
|
1273
|
+
let(:new_person) do
|
1274
|
+
Person.create_with(attrs).find_or_create_by(query)
|
1275
|
+
end
|
1276
|
+
|
1277
|
+
it 'gives the write method args precedence' do
|
1278
|
+
# @todo: uncomment when MONGOID-4193 is closed
|
1279
|
+
#expect(new_person.username).to eq('Beet')
|
1280
|
+
expect(new_person.age).to eq(50)
|
1281
|
+
end
|
1282
|
+
end
|
1283
|
+
end
|
1284
|
+
end
|
1285
|
+
end
|
1286
|
+
|
1287
|
+
context 'when called on a criteria' do
|
1288
|
+
|
1289
|
+
let(:criteria_selector) do
|
1290
|
+
{ 'username' => 'Artichoke', 'age' => 25 }
|
1291
|
+
end
|
1292
|
+
|
1293
|
+
let(:criteria) do
|
1294
|
+
Person.where(criteria_selector)
|
1295
|
+
end
|
1296
|
+
|
1297
|
+
context 'when the original criteria shares attributes with the attribute args' do
|
1298
|
+
|
1299
|
+
context 'when all the original attributes are shared with the new attributes' do
|
1300
|
+
|
1301
|
+
let(:attrs) do
|
1302
|
+
{ 'username' => 'Beet', 'age' => 50 }
|
1303
|
+
end
|
1304
|
+
|
1305
|
+
it 'overwrites all the original attributes' do
|
1306
|
+
expect(criteria.create_with(attrs).selector).to eq(attrs)
|
1307
|
+
end
|
1308
|
+
end
|
1309
|
+
end
|
1310
|
+
|
1311
|
+
context 'when only some of the original attributes are shared with the attribute args' do
|
1312
|
+
|
1313
|
+
let(:attrs) do
|
1314
|
+
{ 'username' => 'Beet' }
|
1315
|
+
end
|
1316
|
+
|
1317
|
+
it 'only overwrites the shared attributes' do
|
1318
|
+
expect(criteria.create_with(attrs).selector).to eq(criteria_selector.merge!(attrs))
|
1319
|
+
end
|
1320
|
+
end
|
1321
|
+
|
1322
|
+
context 'when a method is chained' do
|
1323
|
+
|
1324
|
+
let(:attrs) do
|
1325
|
+
{ 'username' => 'Turnip' }
|
1326
|
+
end
|
1327
|
+
|
1328
|
+
let(:query) do
|
1329
|
+
{ 'username' => 'Beet', 'age' => 50 }
|
1330
|
+
end
|
1331
|
+
|
1332
|
+
context 'when a write method is chained' do
|
1333
|
+
|
1334
|
+
it 'executes the method' do
|
1335
|
+
# @todo: uncomment when MONGOID-4193 is closed
|
1336
|
+
#expect(criteria.create_with(attrs).new.username).to eq('Beet')
|
1337
|
+
expect(criteria.create_with(attrs).new.age).to eq(25)
|
1338
|
+
end
|
1339
|
+
end
|
1340
|
+
|
1341
|
+
context 'when a write method is chained' do
|
1342
|
+
|
1343
|
+
let(:new_person) do
|
1344
|
+
criteria.create_with(attrs).find_or_create_by(query)
|
1345
|
+
end
|
1346
|
+
|
1347
|
+
it 'executes the query' do
|
1348
|
+
# @todo: uncomment when MONGOID-4193 is closed
|
1349
|
+
#expect(new_person.username).to eq('Beet')
|
1350
|
+
#expect(new_person.age).to eq(50)
|
1351
|
+
end
|
1352
|
+
end
|
1353
|
+
end
|
1354
|
+
end
|
1355
|
+
end
|
1230
1356
|
end
|
@@ -1147,12 +1147,72 @@ describe Mongoid::Criteria do
|
|
1147
1147
|
end
|
1148
1148
|
end
|
1149
1149
|
|
1150
|
-
context "when providing a
|
1150
|
+
context "when providing a list of associations" do
|
1151
1151
|
|
1152
|
-
|
1153
|
-
|
1154
|
-
|
1155
|
-
|
1152
|
+
let!(:user) do
|
1153
|
+
User.create(posts: [ post1 ], descriptions: [ description1 ])
|
1154
|
+
end
|
1155
|
+
|
1156
|
+
let!(:post1) do
|
1157
|
+
Post.create
|
1158
|
+
end
|
1159
|
+
|
1160
|
+
let!(:description1) do
|
1161
|
+
Description.create(details: 1)
|
1162
|
+
end
|
1163
|
+
|
1164
|
+
let(:result) do
|
1165
|
+
User.includes(:posts, :descriptions).first
|
1166
|
+
end
|
1167
|
+
|
1168
|
+
it "executes the query" do
|
1169
|
+
expect(result).to eq(user)
|
1170
|
+
end
|
1171
|
+
|
1172
|
+
it "includes the related objects" do
|
1173
|
+
expect(result.posts).to eq([ post1 ])
|
1174
|
+
expect(result.descriptions).to eq([ description1 ])
|
1175
|
+
end
|
1176
|
+
end
|
1177
|
+
|
1178
|
+
context "when providing a nested association" do
|
1179
|
+
|
1180
|
+
let!(:user) do
|
1181
|
+
User.create
|
1182
|
+
end
|
1183
|
+
|
1184
|
+
before do
|
1185
|
+
p = Post.create(alerts: [ Alert.create ])
|
1186
|
+
user.posts = [ p ]
|
1187
|
+
user.save
|
1188
|
+
end
|
1189
|
+
|
1190
|
+
let(:result) do
|
1191
|
+
User.includes(:posts => [:alerts]).first
|
1192
|
+
end
|
1193
|
+
|
1194
|
+
it "executes the query" do
|
1195
|
+
expect(result).to eq(user)
|
1196
|
+
end
|
1197
|
+
|
1198
|
+
it "includes the related objects" do
|
1199
|
+
expect(result.posts.size).to eq(1)
|
1200
|
+
expect(result.posts.first.alerts.size).to eq(1)
|
1201
|
+
end
|
1202
|
+
end
|
1203
|
+
|
1204
|
+
context "when providing a deeply nested association" do
|
1205
|
+
|
1206
|
+
let!(:user) do
|
1207
|
+
User.create
|
1208
|
+
end
|
1209
|
+
|
1210
|
+
let(:results) do
|
1211
|
+
User.includes(:posts => [{ :alerts => :items }]).to_a
|
1212
|
+
end
|
1213
|
+
|
1214
|
+
it "executes the query" do
|
1215
|
+
expect(results.first).to eq(user)
|
1156
1216
|
end
|
1157
1217
|
end
|
1158
1218
|
|
@@ -1759,6 +1819,22 @@ describe Mongoid::Criteria do
|
|
1759
1819
|
person.preferences.create(name: "two")
|
1760
1820
|
end
|
1761
1821
|
|
1822
|
+
context "when one of the related items is deleted" do
|
1823
|
+
|
1824
|
+
before do
|
1825
|
+
person.preferences = [ preference_one, preference_two ]
|
1826
|
+
preference_two.delete
|
1827
|
+
end
|
1828
|
+
|
1829
|
+
let(:criteria) do
|
1830
|
+
Person.where(id: person.id).includes(:preferences)
|
1831
|
+
end
|
1832
|
+
|
1833
|
+
it "only loads the existing related items" do
|
1834
|
+
expect(criteria.entries.first.preferences).to eq([ preference_one ])
|
1835
|
+
end
|
1836
|
+
end
|
1837
|
+
|
1762
1838
|
context "when the criteria has no options" do
|
1763
1839
|
|
1764
1840
|
let!(:criteria) do
|
@@ -441,6 +441,62 @@ describe Mongoid::Document do
|
|
441
441
|
end
|
442
442
|
end
|
443
443
|
|
444
|
+
describe "#as_json" do
|
445
|
+
|
446
|
+
let!(:person) do
|
447
|
+
Person.new(title: "Sir")
|
448
|
+
end
|
449
|
+
|
450
|
+
context "when no options are provided" do
|
451
|
+
|
452
|
+
it "does not apply any options" do
|
453
|
+
expect(person.as_json["title"]).to eq("Sir")
|
454
|
+
expect(person.as_json["age"]).to eq(100)
|
455
|
+
end
|
456
|
+
|
457
|
+
context "when options for the super method are provided" do
|
458
|
+
|
459
|
+
let(:options) do
|
460
|
+
{ only: :title }
|
461
|
+
end
|
462
|
+
|
463
|
+
it "passes the options through to the super method" do
|
464
|
+
expect(person.as_json(options)["title"]).to eq("Sir")
|
465
|
+
expect(person.as_json(options).keys).not_to include("age")
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
context "when the Mongoid-specific options are provided" do
|
471
|
+
|
472
|
+
let(:options) do
|
473
|
+
{ compact: true }
|
474
|
+
end
|
475
|
+
|
476
|
+
it "applies the Mongoid-specific options" do
|
477
|
+
expect(person.as_json(options)["title"]).to eq("Sir")
|
478
|
+
expect(person.as_json(options)["age"]).to eq(100)
|
479
|
+
expect(person.as_json(options).keys).not_to include("lunch_time")
|
480
|
+
end
|
481
|
+
|
482
|
+
context "when options for the super method are provided" do
|
483
|
+
|
484
|
+
let(:options) do
|
485
|
+
{ compact: true, only: [:title, :pets, :ssn] }
|
486
|
+
end
|
487
|
+
|
488
|
+
it "passes the options through to the super method" do
|
489
|
+
expect(person.as_json(options)["title"]).to eq("Sir")
|
490
|
+
expect(person.as_json(options)["pets"]).to eq(false)
|
491
|
+
end
|
492
|
+
|
493
|
+
it "applies the Mongoid-specific options" do
|
494
|
+
expect(person.as_json(options).keys).not_to include("ssn")
|
495
|
+
end
|
496
|
+
end
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
444
500
|
describe "#as_document" do
|
445
501
|
|
446
502
|
let!(:person) do
|
@@ -444,6 +444,29 @@ describe Mongoid::Fields::ForeignKey do
|
|
444
444
|
end
|
445
445
|
end
|
446
446
|
end
|
447
|
+
|
448
|
+
context "when the metadata is polymoprhic" do
|
449
|
+
|
450
|
+
let(:metadata) do
|
451
|
+
Agent.reflect_on_association(:names)
|
452
|
+
end
|
453
|
+
|
454
|
+
let(:field) do
|
455
|
+
described_class.new(:nameable_id, type: Object, metadata: metadata)
|
456
|
+
end
|
457
|
+
|
458
|
+
let(:value) do
|
459
|
+
BSON::ObjectId.new().to_s
|
460
|
+
end
|
461
|
+
|
462
|
+
let(:evolved) do
|
463
|
+
field.evolve(value)
|
464
|
+
end
|
465
|
+
|
466
|
+
it "does not change the foreign key" do
|
467
|
+
expect(evolved).to be(value)
|
468
|
+
end
|
469
|
+
end
|
447
470
|
end
|
448
471
|
|
449
472
|
describe "#lazy?" do
|
@@ -269,32 +269,50 @@ describe Mongoid::Fields::Localized do
|
|
269
269
|
::I18n.fallbacks[:de] = [ :de, :en, :es ]
|
270
270
|
end
|
271
271
|
|
272
|
-
context
|
272
|
+
context 'when fallbacks are enabled' do
|
273
273
|
|
274
|
-
|
275
|
-
field.demongoize({ "en" => 1 })
|
276
|
-
end
|
274
|
+
context "when the first fallback translation exists" do
|
277
275
|
|
278
|
-
|
279
|
-
|
276
|
+
let(:value) do
|
277
|
+
field.demongoize({ "en" => 1 })
|
278
|
+
end
|
279
|
+
|
280
|
+
it "returns the fallback translation" do
|
281
|
+
expect(value).to eq(1)
|
282
|
+
end
|
280
283
|
end
|
281
|
-
end
|
282
284
|
|
283
|
-
|
285
|
+
context "when another fallback translation exists" do
|
284
286
|
|
285
|
-
|
286
|
-
|
287
|
+
let(:value) do
|
288
|
+
field.demongoize({ "es" => 100 })
|
289
|
+
end
|
290
|
+
|
291
|
+
it "returns the fallback translation" do
|
292
|
+
expect(value).to eq(100)
|
293
|
+
end
|
287
294
|
end
|
288
295
|
|
289
|
-
|
290
|
-
|
296
|
+
context "when the fallback translation does not exist" do
|
297
|
+
|
298
|
+
let(:value) do
|
299
|
+
field.demongoize({ "fr" => 50 })
|
300
|
+
end
|
301
|
+
|
302
|
+
it "returns nil" do
|
303
|
+
expect(value).to be_nil
|
304
|
+
end
|
291
305
|
end
|
292
306
|
end
|
293
307
|
|
294
|
-
context
|
308
|
+
context 'when fallbacks are disabled' do
|
309
|
+
|
310
|
+
let(:field) do
|
311
|
+
described_class.new(:description, localize: true, type: Integer, fallbacks: false)
|
312
|
+
end
|
295
313
|
|
296
314
|
let(:value) do
|
297
|
-
field.demongoize({ "
|
315
|
+
field.demongoize({ "es" => 100 })
|
298
316
|
end
|
299
317
|
|
300
318
|
it "returns nil" do
|
@@ -149,7 +149,8 @@ describe Mongoid::Matchable do
|
|
149
149
|
Address.new(
|
150
150
|
services: ["first", "second", "third"],
|
151
151
|
number: 100,
|
152
|
-
map: { key: "value" }
|
152
|
+
map: { key: "value" },
|
153
|
+
street: "Clarkenwell Road"
|
153
154
|
)
|
154
155
|
end
|
155
156
|
|
@@ -303,6 +304,131 @@ describe Mongoid::Matchable do
|
|
303
304
|
end
|
304
305
|
end
|
305
306
|
|
307
|
+
context "with a $not selector" do
|
308
|
+
|
309
|
+
context "regexes" do
|
310
|
+
|
311
|
+
context "when the predicate matches" do
|
312
|
+
|
313
|
+
let(:selector) do
|
314
|
+
{
|
315
|
+
street: {"$not" => /Avenue/}
|
316
|
+
}
|
317
|
+
end
|
318
|
+
|
319
|
+
it "returns true" do
|
320
|
+
expect(document.matches?(selector)).to be true
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
context "when the predicate does not match" do
|
325
|
+
|
326
|
+
let(:selector) do
|
327
|
+
{
|
328
|
+
street: {"$not" => /Road/}
|
329
|
+
}
|
330
|
+
end
|
331
|
+
|
332
|
+
it "returns false" do
|
333
|
+
expect(document.matches?(selector)).to be false
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
context "other operators" do
|
339
|
+
|
340
|
+
context "numerical comparisons" do
|
341
|
+
|
342
|
+
context "$lt and $gt" do
|
343
|
+
|
344
|
+
context "when the predicate matches" do
|
345
|
+
|
346
|
+
let(:selector) do
|
347
|
+
{
|
348
|
+
number: {"$not" => {"$lt" => 0}}
|
349
|
+
}
|
350
|
+
end
|
351
|
+
|
352
|
+
it "returns true" do
|
353
|
+
expect(document.matches?(selector)).to be true
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
context "when the predicate does not match" do
|
358
|
+
|
359
|
+
let(:selector) do
|
360
|
+
{
|
361
|
+
number: {"$not" => {"$gt" => 50}}
|
362
|
+
}
|
363
|
+
end
|
364
|
+
|
365
|
+
it "returns false" do
|
366
|
+
expect(document.matches?(selector)).to be false
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
context "$in" do
|
372
|
+
|
373
|
+
context "when the predicate matches" do
|
374
|
+
|
375
|
+
let(:selector) do
|
376
|
+
{
|
377
|
+
number: {"$not" => {"$in" => [10]}}
|
378
|
+
}
|
379
|
+
end
|
380
|
+
|
381
|
+
it "returns true" do
|
382
|
+
expect(document.matches?(selector)).to be true
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
context "when the predicate does not match" do
|
387
|
+
|
388
|
+
let(:selector) do
|
389
|
+
{
|
390
|
+
number: {"$not" => {"$in" => [100]}}
|
391
|
+
}
|
392
|
+
end
|
393
|
+
|
394
|
+
it "returns false" do
|
395
|
+
expect(document.matches?(selector)).to be false
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
context "symbol keys" do
|
403
|
+
|
404
|
+
context "when the predicate matches" do
|
405
|
+
|
406
|
+
let(:selector) do
|
407
|
+
{
|
408
|
+
street: {:$not => /Avenue/}
|
409
|
+
}
|
410
|
+
end
|
411
|
+
|
412
|
+
it "returns true" do
|
413
|
+
expect(document.matches?(selector)).to be true
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
context "when the predicate does not match" do
|
418
|
+
|
419
|
+
let(:selector) do
|
420
|
+
{
|
421
|
+
street: {:$not => /Road/}
|
422
|
+
}
|
423
|
+
end
|
424
|
+
|
425
|
+
it "returns false" do
|
426
|
+
expect(document.matches?(selector)).to be false
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
306
432
|
context "with an $in selector" do
|
307
433
|
|
308
434
|
context "when the attributes match" do
|