input_sanitizer 0.1.9 → 0.4.0

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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/gempush.yml +28 -0
  3. data/.gitignore +2 -1
  4. data/.travis.yml +4 -8
  5. data/CHANGELOG +96 -0
  6. data/LICENSE +201 -22
  7. data/README.md +22 -3
  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 -52
  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} +30 -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 +213 -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 +460 -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/lib/input_sanitizer/sanitizer.rb +0 -152
  47. data/spec/default_converters_spec.rb +0 -101
  48. data/spec/extended_converters_spec.rb +0 -62
@@ -0,0 +1,98 @@
1
+ require 'spec_helper'
2
+
3
+ class AddressTransform < InputSanitizer::V2::PayloadTransform
4
+ def transform
5
+ rename :line1, :street
6
+ end
7
+ end
8
+
9
+ class TestTransform < InputSanitizer::V2::PayloadTransform
10
+ def transform
11
+ rename :value, :scope
12
+ merge_in :address, :using => AddressTransform
13
+
14
+ if has?(:thing)
15
+ payload[:other_thing] = "123-#{payload.delete(:thing)}"
16
+ end
17
+
18
+ nil
19
+ end
20
+ end
21
+
22
+ class InvalidTransform < InputSanitizer::V2::PayloadTransform
23
+ def call
24
+ end
25
+ end
26
+
27
+ describe InputSanitizer::V2::PayloadTransform do
28
+ let(:payload) do
29
+ {
30
+ :name => 'wat',
31
+ 'value' => 1234,
32
+ :thing => 'zzz',
33
+ 'address' => {
34
+ :line1 => 'wup wup',
35
+ :city => 'Krakow'
36
+ }
37
+ }
38
+ end
39
+
40
+ subject { TestTransform.call(payload) }
41
+
42
+ it "returns the transformed payload" do
43
+ subject[:name].should eq('wat')
44
+ subject[:scope].should eq(1234)
45
+ subject[:other_thing].should eq('123-zzz')
46
+ subject[:street].should eq('wup wup')
47
+ subject[:city].should eq('Krakow')
48
+
49
+ subject[:value].should be_nil
50
+ subject[:line1].should be_nil
51
+ subject[:address].should be_nil
52
+ end
53
+
54
+ context "when attribute is not present" do
55
+ let(:payload) { {} }
56
+
57
+ it "the renamed attribute is not present either" do
58
+ subject.should eq({})
59
+ end
60
+ end
61
+
62
+ context "when attribute is nil" do
63
+ let(:payload) { { :value => nil } }
64
+
65
+ it "renames it correctly" do
66
+ subject.should eq({ 'scope' => nil })
67
+ end
68
+ end
69
+
70
+ context "when there is no data for a nested transform" do
71
+ let(:payload) { { :name => 'wat' } }
72
+
73
+ it "still successfully transforms data" do
74
+ subject.should eq({ 'name' => 'wat' })
75
+ subject[:name].should eq('wat')
76
+ end
77
+ end
78
+
79
+ describe "invalid use of the transform class" do
80
+ subject { InvalidTransform.call(payload) }
81
+
82
+ it "raises an error" do
83
+ lambda { subject }.should raise_error
84
+ end
85
+ end
86
+
87
+ context "when given a frozen InputSanitizer::RestrictedHash" do
88
+ let(:payload) do
89
+ InputSanitizer::RestrictedHash.new([:value]).tap do |hash|
90
+ hash[:value] = 1
91
+ end.freeze
92
+ end
93
+
94
+ it "works" do
95
+ subject.should eq({ 'scope' => 1 })
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,300 @@
1
+ require 'spec_helper'
2
+
3
+ class CustomFieldsSortByQueryFallback
4
+ def self.call(key, direction, context)
5
+ filterable_keys = %w(slt number)
6
+ _, field = key.split(':', 2)
7
+ filterable_keys.include?(field)
8
+ end
9
+ end
10
+
11
+ class TestedQuerySanitizer < InputSanitizer::V2::QuerySanitizer
12
+ string :status, :allow => ['', 'current', 'past']
13
+
14
+ float :float_attribute, :minimum => 1, :maximum => 100, :default => 2.7182
15
+ integer :integer_attribute, :minimum => 1, :maximum => 100, :default => 2
16
+ string :string_attribute
17
+ boolean :bool_attribute
18
+ datetime :datetime_attribute
19
+ url :website
20
+
21
+ integer :ids, :collection => true
22
+ string :tags, :collection => true
23
+ sort_by %w(name updated_at created_at), :default => 'name:asc', :fallback => CustomFieldsSortByQueryFallback
24
+ end
25
+
26
+ class ContextQuerySanitizer < InputSanitizer::V2::QuerySanitizer
27
+ sort_by %w(id created_at updated_at), :fallback => Proc.new { |key, _, context|
28
+ context && context[:allowed] && context[:allowed].include?(key)
29
+ }
30
+ end
31
+
32
+ class ContextForwardingSanitizer < InputSanitizer::V2::PayloadSanitizer
33
+ nested :nested1, :sanitizer => ContextQuerySanitizer
34
+ end
35
+
36
+ describe InputSanitizer::V2::QuerySanitizer do
37
+ let(:sanitizer) { TestedQuerySanitizer.new(@params) }
38
+
39
+ describe 'default value' do
40
+ it 'integer uses a default value if not provided in params' do
41
+ @params = {}
42
+ sanitizer.should be_valid
43
+ sanitizer[:integer_attribute].should eq(2)
44
+ end
45
+
46
+ it 'float uses a default value if not provided in params' do
47
+ @params = {}
48
+ sanitizer.should be_valid
49
+ sanitizer[:float_attribute].should eq(2.7182)
50
+ end
51
+ end
52
+
53
+ describe 'type strictness' do
54
+ it 'is valid if given an integer as a string' do
55
+ @params = { :integer_attribute => '22' }
56
+ sanitizer.should be_valid
57
+ sanitizer[:integer_attribute].should eq(22)
58
+ end
59
+
60
+ it 'is valid if given a float as a string' do
61
+ @params = { :float_attribute => '3.1415' }
62
+ sanitizer.should be_valid
63
+ sanitizer[:float_attribute].should eq(3.1415)
64
+ end
65
+
66
+ it 'is valid if given a float as a string without decimals' do
67
+ @params = { :float_attribute => '3' }
68
+ sanitizer.should be_valid
69
+ sanitizer[:float_attribute].should eq(3.0)
70
+ end
71
+
72
+ it 'is valid if given a "true" boolean as a string' do
73
+ @params = { :bool_attribute => 'true' }
74
+ sanitizer.should be_valid
75
+ sanitizer[:bool_attribute].should eq(true)
76
+ end
77
+
78
+ it 'is valid if given a "false" boolean as a string' do
79
+ @params = { :bool_attribute => 'false' }
80
+ sanitizer.should be_valid
81
+ sanitizer[:bool_attribute].should eq(false)
82
+ end
83
+ end
84
+
85
+ describe "minimum and maximum options" do
86
+ it "is invalid if integer is lower than the minimum" do
87
+ @params = { :integer_attribute => 0 }
88
+ sanitizer.should_not be_valid
89
+ end
90
+
91
+ it "is invalid if integer is greater than the maximum" do
92
+ @params = { :integer_attribute => 101 }
93
+ sanitizer.should_not be_valid
94
+ end
95
+
96
+ it "is valid when integer is within given range" do
97
+ @params = { :integer_attribute => 2 }
98
+ sanitizer.should be_valid
99
+ end
100
+
101
+ it "is invalid if float is lower than the minimum" do
102
+ @params = { :float_attribute => 0.0 }
103
+ sanitizer.should_not be_valid
104
+ end
105
+
106
+ it "is invalid if float is greater than the maximum" do
107
+ @params = { :float_attribute => 101.0 }
108
+ sanitizer.should_not be_valid
109
+ end
110
+
111
+ it "is valid when float is within given range" do
112
+ @params = { :float_attribute => 2.0 }
113
+ sanitizer.should be_valid
114
+ end
115
+ end
116
+
117
+ describe "allow option" do
118
+ it "is valid when given an allowed string" do
119
+ @params = { :status => 'past' }
120
+ sanitizer.should be_valid
121
+ end
122
+
123
+ it "is valid when given an allowed empty string" do
124
+ @params = { :status => '' }
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 "strict param checking" do
136
+ it "is invalid when given extra params" do
137
+ @params = { :extra => 'test', :extra2 => 1 }
138
+ sanitizer.should_not be_valid
139
+ sanitizer.errors.count.should eq(2)
140
+ end
141
+
142
+ it "is valid when given an underscore cache buster" do
143
+ @params = { :_ => '1234567890' }
144
+ sanitizer.should be_valid
145
+ end
146
+ end
147
+
148
+ describe "strict type checking" do
149
+ it "is valid when given an integer" do
150
+ @params = { :integer_attribute => 50 }
151
+ sanitizer.should be_valid
152
+ end
153
+
154
+ it "is valid when given a a float" do
155
+ @params = { :float_attribute => 3.1415 }
156
+ sanitizer.should be_valid
157
+ end
158
+
159
+ it "is valid when given a string" do
160
+ @params = { :string_attribute => '#@!#%#$@#ad' }
161
+ sanitizer.should be_valid
162
+ end
163
+
164
+ it "is invalid when given 'yes' as a bool" do
165
+ @params = { :bool_attribute => 'yes' }
166
+ sanitizer.should_not be_valid
167
+ sanitizer.errors[0].field.should eq('bool_attribute')
168
+ end
169
+
170
+ it "is valid when given true as a bool" do
171
+ @params = { :bool_attribute => true }
172
+ sanitizer.should be_valid
173
+ end
174
+
175
+ it "is valid when given false as a bool" do
176
+ @params = { :bool_attribute => false }
177
+ sanitizer.should be_valid
178
+ end
179
+
180
+ it "is invalid when given an incorrect datetime" do
181
+ @params = { :datetime_attribute => "2014-08-2716:32:56Z" }
182
+ sanitizer.should_not be_valid
183
+ sanitizer.errors[0].field.should eq('datetime_attribute')
184
+ end
185
+
186
+ it "is valid when given a correct datetime" do
187
+ @params = { :datetime_attribute => "2014-08-27T16:32:56Z" }
188
+ sanitizer.should be_valid
189
+ end
190
+
191
+ it "is valid when given a 'forever' timestamp" do
192
+ @params = { :datetime_attribute => "9999-12-31T00:00:00Z" }
193
+ sanitizer.should be_valid
194
+ end
195
+
196
+ it "is valid when given a correct URL" do
197
+ @params = { :website => "https://google.com" }
198
+ sanitizer.should be_valid
199
+ end
200
+
201
+ it "is invalid when given an invalid URL" do
202
+ @params = { :website => "ht:/google.com" }
203
+ sanitizer.should_not be_valid
204
+ end
205
+
206
+ it "is invalid when given an invalid URL that contains a valid URL" do
207
+ @params = { :website => "watwat http://google.com wat" }
208
+ sanitizer.should_not be_valid
209
+ end
210
+ end
211
+
212
+ describe "collections" do
213
+ it "supports comma separated integers" do
214
+ @params = { :ids => "1,2,3" }
215
+ sanitizer.should be_valid
216
+ sanitizer[:ids].should eq([1, 2, 3])
217
+ end
218
+
219
+ it "supports comma separated strings" do
220
+ @params = { :tags => "one,two,three" }
221
+ sanitizer.should be_valid
222
+ sanitizer[:tags].should eq(["one", "two", "three"])
223
+ end
224
+ end
225
+
226
+ describe "sort_by" do
227
+ it "considers default" do
228
+ @params = { }
229
+ sanitizer.should be_valid
230
+ sanitizer[:sort_by].should eq(["name", "asc"])
231
+ end
232
+
233
+ it "accepts correct sorting format" do
234
+ @params = { :sort_by => "updated_at:desc" }
235
+ sanitizer.should be_valid
236
+ sanitizer[:sort_by].should eq(["updated_at", "desc"])
237
+ end
238
+
239
+ it "assumes ascending order by default" do
240
+ @params = { :sort_by => "name" }
241
+ sanitizer.should be_valid
242
+ sanitizer[:sort_by].should eq(["name", "asc"])
243
+ end
244
+
245
+ it "bails to fallback" do
246
+ @params = { :sort_by => 'custom_field:slt:asc' }
247
+ sanitizer.should be_valid
248
+ sanitizer[:sort_by].should eq(["custom_field:slt", "asc"])
249
+ end
250
+
251
+ [
252
+ ['name', true, ["name", "asc"]],
253
+ ['name:asc', true, ["name", "asc"]],
254
+ ['name:desc', true, ["name", "desc"]],
255
+ ['name:', true, ["name", "asc"]],
256
+ ['custom_field:slt', true, ['custom_field:slt', 'asc']],
257
+ ['custom_field:slt:', true, ['custom_field:slt', 'asc']],
258
+ ['custom_field:slt:asc', true, ['custom_field:slt', 'asc']],
259
+ ['custom_field:slt:desc', true, ['custom_field:slt', 'desc']],
260
+ ['unknown', false, nil],
261
+ ['name:invalid', false, nil],
262
+ ['custom_field', false, nil],
263
+ ['custom_field:', false, nil],
264
+ ['custom_field:invalid', false, nil],
265
+ ['custom_field:invalid:asc', false, nil],
266
+ ['custom_field:invalid:desc', false, nil],
267
+ ['custom_field2', false, nil]
268
+ ].each do |sort_param, valid, expectation|
269
+ it "sort by #{sort_param} and returns #{valid}" do
270
+ @params = { :sort_by => sort_param }
271
+ sanitizer.valid?.should eq(valid)
272
+ sanitizer[:sort_by].should eq(expectation)
273
+ end
274
+ end
275
+ end
276
+
277
+ describe 'validation context' do
278
+ let(:sanitizer) { ContextQuerySanitizer.new(@params, @context) }
279
+
280
+ describe 'sort_by' do
281
+ it 'passes context to :fallback' do
282
+ @params = { :sort_by => 'custom_field.external_id' }
283
+ @context = { :allowed => ['custom_field.external_id'] }
284
+ sanitizer.should be_valid
285
+ sanitizer[:sort_by].should eq(["custom_field.external_id", "asc"])
286
+ end
287
+ end
288
+
289
+ describe 'forwarding to nested sanitizers' do
290
+ it 'passes context down' do
291
+ params = { :nested1 => { :sort_by => 'custom_field.external_id' } }
292
+ context = { :allowed => ['custom_field.external_id'] }
293
+ sanitizer = ContextForwardingSanitizer.new(params, context)
294
+
295
+ sanitizer.should be_valid
296
+ sanitizer[:nested1].should eq(:sort_by => ["custom_field.external_id", "asc"])
297
+ end
298
+ end
299
+ end
300
+ end
data/v2.md ADDED
@@ -0,0 +1,52 @@
1
+ # InputSanitizer::V2::PayloadSanitizer
2
+
3
+ Usage example:
4
+
5
+ ```ruby
6
+ class ContactPayload < InputSanitizer::V2::PayloadSanitizer
7
+ string :status, allow: ['', 'current', 'past']
8
+ integer :ids, collection: true, minimum: 1
9
+ string :tags, collection: { minimum: 1, maximum: 4 }
10
+ boolean :admin_flag
11
+ datetime :launch_at
12
+ url :website
13
+ nested :address, sanitizer: AddressSanitizer
14
+ end
15
+
16
+ class AddressSanitizer < InputSanitizer::V2::PayloadSanitizer
17
+ string :city
18
+ end
19
+ ```
20
+
21
+ # InputSanitizer::V2::QuerySanitizer
22
+
23
+ Example:
24
+
25
+ ```ruby
26
+ class IndexParams < InputSanitizer::V2::QuerySanitizer
27
+ string :name
28
+ integer :ids, collection: true
29
+ sort_by %w(name updated_at created_at)
30
+ end
31
+ ```
32
+
33
+ # InputSanitizer::V2::PayloadTransform
34
+
35
+ Example:
36
+
37
+ ```ruby
38
+ class AddressTransform < InputSanitizer::V2::PayloadTransform
39
+ def transform
40
+ rename :line1, :street
41
+ end
42
+ end
43
+
44
+ class ContactPayloadTransform < InputSanitizer::V2::PayloadTransform
45
+ def transform
46
+ rename :value, :scope
47
+ merge_in :address, using: AddressTransform
48
+
49
+ payload[:other_thing] = payload.delete(:thing) * 2
50
+ end
51
+ end
52
+ ```