input_sanitizer 0.1.10 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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