input_sanitizer 0.1.10 → 0.4.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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yaml +26 -0
  3. data/.github/workflows/gempush.yml +28 -0
  4. data/.gitignore +2 -1
  5. data/CHANGELOG +99 -0
  6. data/LICENSE +201 -22
  7. data/README.md +24 -4
  8. data/input_sanitizer.gemspec +10 -4
  9. data/lib/input_sanitizer.rb +5 -2
  10. data/lib/input_sanitizer/errors.rb +142 -0
  11. data/lib/input_sanitizer/extended_converters.rb +5 -42
  12. data/lib/input_sanitizer/extended_converters/comma_joined_integers_converter.rb +15 -0
  13. data/lib/input_sanitizer/extended_converters/comma_joined_strings_converter.rb +15 -0
  14. data/lib/input_sanitizer/extended_converters/positive_integer_converter.rb +12 -0
  15. data/lib/input_sanitizer/extended_converters/specific_values_converter.rb +19 -0
  16. data/lib/input_sanitizer/restricted_hash.rb +49 -8
  17. data/lib/input_sanitizer/v1.rb +22 -0
  18. data/lib/input_sanitizer/v1/clean_field.rb +38 -0
  19. data/lib/input_sanitizer/{default_converters.rb → v1/default_converters.rb} +20 -13
  20. data/lib/input_sanitizer/v1/sanitizer.rb +166 -0
  21. data/lib/input_sanitizer/v2.rb +13 -0
  22. data/lib/input_sanitizer/v2/clean_field.rb +36 -0
  23. data/lib/input_sanitizer/v2/clean_payload_collection_field.rb +41 -0
  24. data/lib/input_sanitizer/v2/clean_query_collection_field.rb +40 -0
  25. data/lib/input_sanitizer/v2/error_collection.rb +49 -0
  26. data/lib/input_sanitizer/v2/nested_sanitizer_factory.rb +19 -0
  27. data/lib/input_sanitizer/v2/payload_sanitizer.rb +130 -0
  28. data/lib/input_sanitizer/v2/payload_transform.rb +42 -0
  29. data/lib/input_sanitizer/v2/query_sanitizer.rb +33 -0
  30. data/lib/input_sanitizer/v2/types.rb +227 -0
  31. data/lib/input_sanitizer/version.rb +1 -1
  32. data/spec/extended_converters/comma_joined_integers_converter_spec.rb +18 -0
  33. data/spec/extended_converters/comma_joined_strings_converter_spec.rb +18 -0
  34. data/spec/extended_converters/positive_integer_converter_spec.rb +18 -0
  35. data/spec/extended_converters/specific_values_converter_spec.rb +27 -0
  36. data/spec/restricted_hash_spec.rb +37 -7
  37. data/spec/sanitizer_spec.rb +129 -26
  38. data/spec/spec_helper.rb +17 -2
  39. data/spec/v1/default_converters_spec.rb +141 -0
  40. data/spec/v2/converters_spec.rb +174 -0
  41. data/spec/v2/payload_sanitizer_spec.rb +534 -0
  42. data/spec/v2/payload_transform_spec.rb +98 -0
  43. data/spec/v2/query_sanitizer_spec.rb +300 -0
  44. data/v2.md +52 -0
  45. metadata +105 -40
  46. data/.travis.yml +0 -12
  47. data/lib/input_sanitizer/sanitizer.rb +0 -140
  48. data/spec/default_converters_spec.rb +0 -101
  49. data/spec/extended_converters_spec.rb +0 -62
@@ -0,0 +1,534 @@
1
+ require 'spec_helper'
2
+
3
+ class AddressSanitizer < InputSanitizer::V2::PayloadSanitizer
4
+ string :city
5
+ string :zip
6
+ end
7
+
8
+ class TagSanitizer < InputSanitizer::V2::PayloadSanitizer
9
+ integer :id
10
+ string :name
11
+ nested :addresses, :sanitizer => AddressSanitizer, :collection => true
12
+ end
13
+
14
+ class TestedPayloadSanitizer < InputSanitizer::V2::PayloadSanitizer
15
+ integer :array, :collection => true
16
+ integer :array_nil, :collection => true, :allow_nil => true
17
+ string :status, :allow => ['current', 'past']
18
+ string :status_with_empty, :allow => ['', 'current', 'past']
19
+ string :regexp_string, :regexp => /^#?([a-f0-9]{6}|[a-f0-9]{3})$/
20
+ string :utf8mb4_string, :strip_4byte_chars => true
21
+ string :value_restricted_utf8mb4_string, :strip_4byte_chars => true, :allow => ['test']
22
+ string :non_blank_utf8mb4_string, :strip_4byte_chars => true, :allow_blank => false
23
+ string :size_restricted_utf8mb4_string, :strip_4byte_chars => true, :minimum => 2, :maximum => 4
24
+ nested :address, :sanitizer => AddressSanitizer
25
+ nested :nullable_address, :sanitizer => AddressSanitizer, :allow_nil => true
26
+ nested :tags, :sanitizer => TagSanitizer, :collection => true
27
+
28
+ float :float_attribute, :minimum => 1, :maximum => 100
29
+ integer :integer_attribute, :minimum => 1, :maximum => 100
30
+ string :string_attribute
31
+ boolean :bool_attribute
32
+ datetime :datetime_attribute
33
+ date :date_attribute, :minimum => Date.new(-2015, 01, 01), :maximum => Date.new(2015, 12, 31)
34
+
35
+ url :website
36
+ string :limited_collection, :collection => { :minimum => 1, :maximum => 2 }, :minimum => 2, :maximum => 12
37
+ string :allow_collection, :collection => true, :allow => ['yes', 'no']
38
+ end
39
+
40
+ class BlankValuesPayloadSanitizer < InputSanitizer::V2::PayloadSanitizer
41
+ string :required_string, :required => true
42
+ datetime :non_nil_datetime, :allow_nil => false
43
+ url :non_blank_url, :allow_blank => false
44
+ end
45
+
46
+ class CustomConverterWithProvidedValue < InputSanitizer::V2::PayloadSanitizer
47
+ integer :from
48
+ custom :to, :provide => :from, :converter => lambda { |value, options|
49
+ InputSanitizer::V2::Types::IntegerCheck.new.call(value)
50
+ raise InputSanitizer::ValueError.new(value, options[:provided][:from], nil) if options[:provided][:from] > value
51
+ value
52
+ }
53
+ end
54
+
55
+ describe InputSanitizer::V2::PayloadSanitizer do
56
+ let(:sanitizer) { TestedPayloadSanitizer.new(@params) }
57
+ let(:cleaned) { sanitizer.cleaned }
58
+
59
+ describe "collections" do
60
+ it "is invalid if collection is not an array" do
61
+ @params = { :array => {} }
62
+ sanitizer.should_not be_valid
63
+ end
64
+
65
+ it "is valid if collection is an array" do
66
+ @params = { :array => [] }
67
+ sanitizer.should be_valid
68
+ end
69
+
70
+ it "is valid if collection is an nil and allow_nil is passed" do
71
+ @params = { :array_nil => nil }
72
+ sanitizer.should be_valid
73
+ end
74
+
75
+ it "is invalid if there are too few elements" do
76
+ @params = { :limited_collection => [] }
77
+ sanitizer.should_not be_valid
78
+ end
79
+
80
+ it "is invalid if there are too many elements" do
81
+ @params = { :limited_collection => ['bear', 'bear', 'bear'] }
82
+ sanitizer.should_not be_valid
83
+ end
84
+
85
+ it "is valid when there are just enough elements" do
86
+ @params = { :limited_collection => ['goldilocks'] }
87
+ sanitizer.should be_valid
88
+ end
89
+
90
+ it "is invalid when any of the elements are too long" do
91
+ @params = { :limited_collection => ['more_than_the_limit'] }
92
+ sanitizer.should_not be_valid
93
+ end
94
+
95
+ it "is invalid when any of the elements are too short" do
96
+ @params = { :limited_collection => ['a'] }
97
+ sanitizer.should_not be_valid
98
+ end
99
+
100
+ it "is invalid when given disallowed value in a collection" do
101
+ @params = { :allow_collection => ['yes', 'no', 'whoa'] }
102
+ sanitizer.should_not be_valid
103
+ end
104
+
105
+ it "is valid when given allowed values in a collection" do
106
+ @params = { :allow_collection => ['yes', 'no'] }
107
+ sanitizer.should be_valid
108
+ end
109
+ end
110
+
111
+ describe "allow option" do
112
+ it "is valid when given an allowed string" do
113
+ @params = { :status => 'past' }
114
+ sanitizer.should be_valid
115
+ end
116
+
117
+ it "is invalid when given an empty string" do
118
+ @params = { :status => '' }
119
+ sanitizer.should_not be_valid
120
+ sanitizer.errors[0].field.should eq('/status')
121
+ end
122
+
123
+ it "is valid when given an allowed empty string" do
124
+ @params = { :status_with_empty => '' }
125
+ sanitizer.should be_valid
126
+ end
127
+
128
+ it "is invalid when given a disallowed string" do
129
+ @params = { :status => 'current bad string' }
130
+ sanitizer.should_not be_valid
131
+ sanitizer.errors[0].field.should eq('/status')
132
+ end
133
+ end
134
+
135
+ describe "minimum and maximum options" do
136
+ it "is invalid if integer is lower than the minimum" do
137
+ @params = { :integer_attribute => 0 }
138
+ sanitizer.should_not be_valid
139
+ end
140
+
141
+ it "is invalid if integer is greater than the maximum" do
142
+ @params = { :integer_attribute => 101 }
143
+ sanitizer.should_not be_valid
144
+ end
145
+
146
+ it "is valid when integer is within given range" do
147
+ @params = { :limited_collection => ['goldilocks'] }
148
+ sanitizer.should be_valid
149
+ end
150
+
151
+ it "is invalid if float is lower than the minimum" do
152
+ @params = { :float_attribute => 0.0 }
153
+ sanitizer.should_not be_valid
154
+ end
155
+
156
+ it "is invalid if float is greater than the maximum" do
157
+ @params = { :float_attribute => 101.0 }
158
+ sanitizer.should_not be_valid
159
+ end
160
+ end
161
+
162
+ describe "strip_4byte_chars option" do
163
+ it "is valid when given a string with 4-byte chars" do
164
+ @params = { :utf8mb4_string => "test \u{1F435} value" }
165
+ sanitizer.should be_valid
166
+ end
167
+
168
+ it "returns sanitized string without 4-byte chars" do
169
+ @params = { :utf8mb4_string => "test\u{1F435}" }
170
+ sanitizer[:utf8mb4_string].should eq "test"
171
+ end
172
+
173
+ it "properly handles string with 4-byte char at the beginning" do
174
+ @params = { :utf8mb4_string => "\u{1F435} 4-byte char at the beginning" }
175
+ sanitizer[:utf8mb4_string].should eq ' 4-byte char at the beginning'
176
+ end
177
+
178
+ it "properly handles string with 4-byte char in the middle" do
179
+ @params = { :utf8mb4_string => "4-byte char\u{1F435} in the middle" }
180
+ sanitizer[:utf8mb4_string].should eq '4-byte char in the middle'
181
+ end
182
+
183
+ it "properly handles string with 4-byte char at the end" do
184
+ @params = { :utf8mb4_string => "4-byte char at the end \u{1F435}" }
185
+ sanitizer[:utf8mb4_string].should eq '4-byte char at the end '
186
+ end
187
+
188
+ it "does not strip 3-byte chars" do
189
+ @params = { :utf8mb4_string => "Test \u{270A}" }
190
+ sanitizer[:utf8mb4_string].should eq "Test \u{270A}"
191
+ end
192
+
193
+ describe "when used with other options" do
194
+ describe "allow" do
195
+ it "is valid when string matches any value in allowlist before stripping 4-byte chars" do
196
+ @params = { :value_restricted_utf8mb4_string => "test" }
197
+ sanitizer.should be_valid
198
+ end
199
+
200
+ it "is invalid when string doesn't match any value in allowlist before stripping 4-byte chars" do
201
+ @params = { :value_restricted_utf8mb4_string => "test\u{1F435}" }
202
+ sanitizer.should_not be_valid
203
+ end
204
+ end
205
+
206
+ describe "allow_blank=false" do
207
+ it "is invalid when string is already blank before stripping 4-byte chars" do
208
+ @params = { :non_blank_utf8mb4_string => " " }
209
+ sanitizer.should_not be_valid
210
+ end
211
+
212
+ it "is invalid when string becomes blank as a result of stripping 4-byte chars" do
213
+ @params = { :non_blank_utf8mb4_string => " \u{1F435} " }
214
+ sanitizer.should_not be_valid
215
+ end
216
+ end
217
+
218
+ describe "minimum and maximum" do
219
+ it "is invalid when string is already too long before stripping 4-byte chars" do
220
+ @params = { :size_restricted_utf8mb4_string => "1234\u{1F435}" }
221
+ sanitizer.should_not be_valid
222
+ end
223
+
224
+ it "is invalid when string becomes too short as a result of stripping 4-byte chars" do
225
+ @params = { :size_restricted_utf8mb4_string => "1\u{1F435}" }
226
+ sanitizer.should_not be_valid
227
+ end
228
+ end
229
+ end
230
+ end
231
+
232
+ describe "strict param checking" do
233
+ it "is invalid when given extra params" do
234
+ @params = { :extra => 'test', :extra2 => 1 }
235
+ sanitizer.should_not be_valid
236
+ sanitizer.errors.count.should eq(2)
237
+ end
238
+
239
+ it "is invalid when given extra params in a nested sanitizer" do
240
+ @params = { :address => { :extra => 0 }, :tags => [ { :extra2 => 1 } ] }
241
+ sanitizer.should_not be_valid
242
+ sanitizer.errors.map(&:field).should contain_exactly('/address/extra', '/tags/0/extra2')
243
+ end
244
+ end
245
+
246
+ describe "converters with provided values" do
247
+ let(:sanitizer) { CustomConverterWithProvidedValue.new(@params) }
248
+
249
+ it "is valid when converter passes check with provided value" do
250
+ @params = {from: 1, to: 3}
251
+ sanitizer.should be_valid
252
+ end
253
+
254
+ it "is invalid when converter does not pass check with provided value" do
255
+ @params = {from: 3, to: 1}
256
+ sanitizer.should_not be_valid
257
+ end
258
+ end
259
+
260
+ describe "strict type checking" do
261
+ it "is invalid when given string instead of integer" do
262
+ @params = { :integer_attribute => '1' }
263
+ sanitizer.should_not be_valid
264
+ sanitizer.errors[0].field.should eq('/integer_attribute')
265
+ end
266
+
267
+ it "is valid when given an integer" do
268
+ @params = { :integer_attribute => 50 }
269
+ sanitizer.should be_valid
270
+ sanitizer[:integer_attribute].should eq(50)
271
+ end
272
+
273
+ it "is invalid when given a float" do
274
+ @params = { :integer_attribute => 50.99 }
275
+ sanitizer.should_not be_valid
276
+ end
277
+
278
+ it "is valid when given nil for an integer" do
279
+ @params = { :integer_attribute => nil }
280
+ sanitizer.should be_valid
281
+ sanitizer[:integer_attribute].should be_nil
282
+ end
283
+
284
+ it "is invalid when given string instead of float" do
285
+ @params = { :float_attribute => '1' }
286
+ sanitizer.should_not be_valid
287
+ sanitizer.errors[0].field.should eq('/float_attribute')
288
+ end
289
+
290
+ it "is valid when given a float" do
291
+ @params = { :float_attribute => 50.0 }
292
+ sanitizer.should be_valid
293
+ sanitizer[:float_attribute].should eq(50.0)
294
+ end
295
+
296
+ it "is valid when given nil for a float" do
297
+ @params = { :float_attribute => nil }
298
+ sanitizer.should be_valid
299
+ sanitizer[:float_attribute].should be_nil
300
+ end
301
+
302
+ it "is valid when given string is matching regexp" do
303
+ @params = { :regexp_string => "#8bd635" }
304
+ sanitizer.should be_valid
305
+ sanitizer[:regexp_string].should eq('#8bd635')
306
+ end
307
+
308
+ it "is invalid when given string is not matching regexp" do
309
+ @params = { :regexp_string => "not a hex value" }
310
+ sanitizer.should_not be_valid
311
+ sanitizer.errors[0].field.should eq('/regexp_string')
312
+ end
313
+
314
+ it "is invalid when given integer instead of string" do
315
+ @params = { :string_attribute => 0 }
316
+ sanitizer.should_not be_valid
317
+ sanitizer.errors[0].field.should eq('/string_attribute')
318
+ end
319
+
320
+ it "is invalid when given float instead of string" do
321
+ @params = { :string_attribute => 3.1415 }
322
+ sanitizer.should_not be_valid
323
+ sanitizer.errors[0].field.should eq('/string_attribute')
324
+ end
325
+
326
+ it "is valid when given a string" do
327
+ @params = { :string_attribute => '#@!#%#$@#ad' }
328
+ sanitizer.should be_valid
329
+ sanitizer[:string_attribute].should eq('#@!#%#$@#ad')
330
+ end
331
+
332
+ it "is invalid when given 'yes' as a bool" do
333
+ @params = { :bool_attribute => 'yes' }
334
+ sanitizer.should_not be_valid
335
+ sanitizer.errors[0].field.should eq('/bool_attribute')
336
+ end
337
+
338
+ it "is valid when given true as a bool" do
339
+ @params = { :bool_attribute => true }
340
+ sanitizer.should be_valid
341
+ end
342
+
343
+ it "is valid when given false as a bool" do
344
+ @params = { :bool_attribute => false }
345
+ sanitizer.should be_valid
346
+ end
347
+
348
+ it "is invalid when given an incorrect datetime" do
349
+ @params = { :datetime_attribute => "2014-08-2716:32:56Z" }
350
+ sanitizer.should_not be_valid
351
+ sanitizer.errors[0].field.should eq('/datetime_attribute')
352
+ end
353
+
354
+ it "is valid when given a correct datetime" do
355
+ @params = { :datetime_attribute => "2014-08-27T16:32:56Z" }
356
+ sanitizer.should be_valid
357
+ end
358
+
359
+ it "is valid when given a 'forever' timestamp" do
360
+ @params = { :datetime_attribute => "9999-12-31T00:00:00Z" }
361
+ sanitizer.should be_valid
362
+ end
363
+
364
+ it "is invalid when given an incorrect date" do
365
+ @params = { :date_attribute => "invalid" }
366
+ sanitizer.should_not be_valid
367
+ sanitizer.errors[0].field.should eq('/date_attribute')
368
+ end
369
+
370
+ it "is valid when given a correct date" do
371
+ @params = { :date_attribute => "2015-08-27" }
372
+ sanitizer.should be_valid
373
+ end
374
+
375
+ it "is valid when given a correct negative date" do
376
+ @params = { :date_attribute => "-2014-08-27" }
377
+ sanitizer.should be_valid
378
+ end
379
+
380
+ it "is valid when given a correct URL" do
381
+ @params = { :website => "https://google.com" }
382
+ sanitizer.should be_valid
383
+ sanitizer[:website].should eq("https://google.com")
384
+ end
385
+
386
+ it "is invalid when given an invalid URL" do
387
+ @params = { :website => "ht:/google.com" }
388
+ sanitizer.should_not be_valid
389
+ end
390
+
391
+ it "is invalid when given an invalid URL that contains a valid URL" do
392
+ @params = { :website => "watwat http://google.com wat" }
393
+ sanitizer.should_not be_valid
394
+ end
395
+
396
+ describe "blank and required values" do
397
+ let(:sanitizer) { BlankValuesPayloadSanitizer.new(@params) }
398
+ let(:defaults) { { :required_string => 'zz' } }
399
+
400
+ it "is invalid if required string is missing" do
401
+ @params = {}
402
+ sanitizer.should_not be_valid
403
+ sanitizer.errors[0].should be_an_instance_of(InputSanitizer::ValueMissingError)
404
+ sanitizer.errors[0].field.should eq('/required_string')
405
+ end
406
+
407
+ it "is invalid if required string is nil" do
408
+ @params = { :required_string => nil }
409
+ sanitizer.should_not be_valid
410
+ sanitizer.errors[0].should be_an_instance_of(InputSanitizer::BlankValueError)
411
+ sanitizer.errors[0].field.should eq('/required_string')
412
+ end
413
+
414
+ it "is invalid if required string is blank" do
415
+ @params = { :required_string => ' ' }
416
+ sanitizer.should_not be_valid
417
+ sanitizer.errors[0].should be_an_instance_of(InputSanitizer::BlankValueError)
418
+ sanitizer.errors[0].field.should eq('/required_string')
419
+ end
420
+
421
+ it "is invalid if non-nil datetime is null" do
422
+ @params = defaults.merge({ :non_nil_datetime => nil })
423
+ sanitizer.should_not be_valid
424
+ sanitizer.errors[0].should be_an_instance_of(InputSanitizer::BlankValueError)
425
+ sanitizer.errors[0].field.should eq('/non_nil_datetime')
426
+ end
427
+
428
+ it "is valid if non-nil datetime is blank" do
429
+ @params = defaults.merge({ :non_nil_datetime => '' })
430
+ sanitizer.should be_valid
431
+ end
432
+
433
+ it "is invalid if non-blank url is nil" do
434
+ @params = defaults.merge({ :non_blank_url => nil })
435
+ sanitizer.should_not be_valid
436
+ sanitizer.errors[0].should be_an_instance_of(InputSanitizer::BlankValueError)
437
+ sanitizer.errors[0].field.should eq('/non_blank_url')
438
+ end
439
+
440
+ it "is invalid if non-blank url is blank" do
441
+ @params = defaults.merge({ :non_blank_url => '' })
442
+ sanitizer.should_not be_valid
443
+ sanitizer.errors[0].should be_an_instance_of(InputSanitizer::BlankValueError)
444
+ sanitizer.errors[0].field.should eq('/non_blank_url')
445
+ end
446
+ end
447
+
448
+ describe "nested checking" do
449
+ describe "simple array" do
450
+ it "returns JSON pointer for invalid fields" do
451
+ @params = { :array => [1, 'z', '3', 4] }
452
+ sanitizer.errors.length.should eq(2)
453
+ sanitizer.errors.map(&:field).should contain_exactly('/array/1', '/array/2')
454
+ end
455
+ end
456
+
457
+ describe "nested object" do
458
+ it "returns an error when given a nil for a nested value" do
459
+ @params = { :address => nil }
460
+ sanitizer.should_not be_valid
461
+ end
462
+
463
+ it "returns an error when given a string for a nested value" do
464
+ @params = { :address => 'nope' }
465
+ sanitizer.should_not be_valid
466
+ end
467
+
468
+ it "returns an error when given an array for a nested value" do
469
+ @params = { :address => ['a'] }
470
+ sanitizer.should_not be_valid
471
+ end
472
+
473
+ it "returns JSON pointer for invalid fields" do
474
+ @params = { :address => { :city => 0, :zip => 1 } }
475
+ sanitizer.errors.length.should eq(2)
476
+ sanitizer.errors.map(&:field).should contain_exactly('/address/city', '/address/zip')
477
+ end
478
+
479
+ it "allows nil with `allow_nil` flag" do
480
+ @params = { :nullable_address => nil }
481
+ sanitizer.should be_valid
482
+ sanitizer.cleaned.fetch(:nullable_address).should eq(nil)
483
+ end
484
+ end
485
+
486
+ describe "array of nested objects" do
487
+ it "returns an error when given a nil for a collection" do
488
+ @params = { :tags => nil }
489
+ sanitizer.should_not be_valid
490
+ end
491
+
492
+ it "returns an error when given a string for a collection" do
493
+ @params = { :tags => 'nope' }
494
+ sanitizer.should_not be_valid
495
+ end
496
+
497
+ it "returns an error when given a hash for a collection" do
498
+ @params = { :tags => { :a => 1 } }
499
+ sanitizer.should_not be_valid
500
+ end
501
+
502
+ it "returns JSON pointer for invalid fields" do
503
+ @params = { :tags => [ { :id => 'n', :name => 1 }, { :id => 10, :name => 2 } ] }
504
+ sanitizer.errors.length.should eq(3)
505
+ sanitizer.errors.map(&:field).should contain_exactly(
506
+ '/tags/0/id',
507
+ '/tags/0/name',
508
+ '/tags/1/name'
509
+ )
510
+ end
511
+ end
512
+
513
+ describe "array of nested objects that have array of nested objects" do
514
+ it "returns JSON pointer for invalid fields" do
515
+ @params = { :tags => [
516
+ { :id => 'n', :addresses => [ { :city => 0 }, { :city => 1 } ] },
517
+ { :name => 2, :addresses => [ { :city => 3 } ] },
518
+ ] }
519
+ sanitizer.errors.length.should eq(5)
520
+ sanitizer.errors.map(&:field).should contain_exactly(
521
+ '/tags/0/id',
522
+ '/tags/0/addresses/0/city',
523
+ '/tags/0/addresses/1/city',
524
+ '/tags/1/name',
525
+ '/tags/1/addresses/0/city'
526
+ )
527
+
528
+ ec = sanitizer.error_collection
529
+ ec.length.should eq(5)
530
+ end
531
+ end
532
+ end
533
+ end
534
+ end