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,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
+ ```