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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yaml +26 -0
- data/.github/workflows/gempush.yml +28 -0
- data/.gitignore +2 -1
- data/CHANGELOG +99 -0
- data/LICENSE +201 -22
- data/README.md +24 -4
- data/input_sanitizer.gemspec +10 -4
- data/lib/input_sanitizer.rb +5 -2
- data/lib/input_sanitizer/errors.rb +142 -0
- data/lib/input_sanitizer/extended_converters.rb +5 -42
- data/lib/input_sanitizer/extended_converters/comma_joined_integers_converter.rb +15 -0
- data/lib/input_sanitizer/extended_converters/comma_joined_strings_converter.rb +15 -0
- data/lib/input_sanitizer/extended_converters/positive_integer_converter.rb +12 -0
- data/lib/input_sanitizer/extended_converters/specific_values_converter.rb +19 -0
- data/lib/input_sanitizer/restricted_hash.rb +49 -8
- data/lib/input_sanitizer/v1.rb +22 -0
- data/lib/input_sanitizer/v1/clean_field.rb +38 -0
- data/lib/input_sanitizer/{default_converters.rb → v1/default_converters.rb} +20 -13
- data/lib/input_sanitizer/v1/sanitizer.rb +166 -0
- data/lib/input_sanitizer/v2.rb +13 -0
- data/lib/input_sanitizer/v2/clean_field.rb +36 -0
- data/lib/input_sanitizer/v2/clean_payload_collection_field.rb +41 -0
- data/lib/input_sanitizer/v2/clean_query_collection_field.rb +40 -0
- data/lib/input_sanitizer/v2/error_collection.rb +49 -0
- data/lib/input_sanitizer/v2/nested_sanitizer_factory.rb +19 -0
- data/lib/input_sanitizer/v2/payload_sanitizer.rb +130 -0
- data/lib/input_sanitizer/v2/payload_transform.rb +42 -0
- data/lib/input_sanitizer/v2/query_sanitizer.rb +33 -0
- data/lib/input_sanitizer/v2/types.rb +227 -0
- data/lib/input_sanitizer/version.rb +1 -1
- data/spec/extended_converters/comma_joined_integers_converter_spec.rb +18 -0
- data/spec/extended_converters/comma_joined_strings_converter_spec.rb +18 -0
- data/spec/extended_converters/positive_integer_converter_spec.rb +18 -0
- data/spec/extended_converters/specific_values_converter_spec.rb +27 -0
- data/spec/restricted_hash_spec.rb +37 -7
- data/spec/sanitizer_spec.rb +129 -26
- data/spec/spec_helper.rb +17 -2
- data/spec/v1/default_converters_spec.rb +141 -0
- data/spec/v2/converters_spec.rb +174 -0
- data/spec/v2/payload_sanitizer_spec.rb +534 -0
- data/spec/v2/payload_transform_spec.rb +98 -0
- data/spec/v2/query_sanitizer_spec.rb +300 -0
- data/v2.md +52 -0
- metadata +105 -40
- data/.travis.yml +0 -12
- data/lib/input_sanitizer/sanitizer.rb +0 -140
- data/spec/default_converters_spec.rb +0 -101
- 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
|
+
```
|