file_validators 2.0.2 → 3.0.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +32 -0
  4. data/.tool-versions +1 -0
  5. data/.travis.yml +33 -5
  6. data/Appraisals +16 -10
  7. data/CHANGELOG.md +26 -0
  8. data/Gemfile +2 -0
  9. data/README.md +32 -21
  10. data/Rakefile +3 -1
  11. data/file_validators.gemspec +13 -7
  12. data/gemfiles/activemodel_3.2.gemfile +2 -1
  13. data/gemfiles/activemodel_4.0.gemfile +2 -1
  14. data/gemfiles/activemodel_4.1.gemfile +1 -0
  15. data/gemfiles/activemodel_4.2.gemfile +2 -1
  16. data/gemfiles/{activemodel_3.0.gemfile → activemodel_5.0.gemfile} +1 -1
  17. data/gemfiles/{activemodel_3.1.gemfile → activemodel_5.2.gemfile} +1 -1
  18. data/lib/file_validators.rb +11 -2
  19. data/lib/file_validators/error.rb +6 -0
  20. data/lib/file_validators/locale/en.yml +0 -2
  21. data/lib/file_validators/mime_type_analyzer.rb +106 -0
  22. data/lib/file_validators/validators/file_content_type_validator.rb +37 -42
  23. data/lib/file_validators/validators/file_size_validator.rb +62 -19
  24. data/lib/file_validators/version.rb +3 -1
  25. data/spec/integration/combined_validators_integration_spec.rb +3 -1
  26. data/spec/integration/file_content_type_validation_integration_spec.rb +117 -11
  27. data/spec/integration/file_size_validator_integration_spec.rb +100 -10
  28. data/spec/lib/file_validators/mime_type_analyzer_spec.rb +139 -0
  29. data/spec/lib/file_validators/validators/file_content_type_validator_spec.rb +93 -36
  30. data/spec/lib/file_validators/validators/file_size_validator_spec.rb +87 -34
  31. data/spec/spec_helper.rb +9 -1
  32. data/spec/support/fakeio.rb +17 -0
  33. data/spec/support/helpers.rb +7 -0
  34. data/spec/support/matchers/allow_content_type.rb +2 -0
  35. data/spec/support/matchers/allow_file_size.rb +2 -0
  36. metadata +87 -27
  37. data/lib/file_validators/utils/content_type_detector.rb +0 -46
  38. data/lib/file_validators/utils/media_type_spoof_detector.rb +0 -46
  39. data/spec/lib/file_validators/utils/content_type_detector_spec.rb +0 -27
  40. data/spec/lib/file_validators/utils/media_type_spoof_detector_spec.rb +0 -31
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
  require 'rack/test/uploaded_file'
3
5
 
@@ -44,7 +46,7 @@ describe 'File Size Validator integration with ActiveModel' do
44
46
  before :all do
45
47
  Person.class_eval do
46
48
  Person.reset_callbacks(:validate)
47
- validates :avatar, file_size: { in: lambda { |record| 20.kilobytes..40.kilobytes } }
49
+ validates :avatar, file_size: { in: ->(_record) { 20.kilobytes..40.kilobytes } }
48
50
  end
49
51
  end
50
52
 
@@ -99,8 +101,8 @@ describe 'File Size Validator integration with ActiveModel' do
99
101
  before :all do
100
102
  Person.class_eval do
101
103
  Person.reset_callbacks(:validate)
102
- validates :avatar, file_size: { greater_than: lambda { |record| 20.kilobytes },
103
- less_than: lambda { |record| 40.kilobytes } }
104
+ validates :avatar, file_size: { greater_than: ->(_record) { 20.kilobytes },
105
+ less_than: ->(_record) { 40.kilobytes } }
104
106
  end
105
107
  end
106
108
 
@@ -218,24 +220,35 @@ describe 'File Size Validator integration with ActiveModel' do
218
220
  subject { Person.new }
219
221
 
220
222
  context 'when file size is less than the specified size' do
221
- before { subject.avatar = "{\"filename\":\"img140910_88338.GIF\",\"content_type\":\"image/gif\",\"size\":13150}" }
223
+ before do
224
+ subject.avatar = '{"filename":"img140910_88338.GIF","content_type":"image/gif","size":13150}'
225
+ end
226
+
222
227
  it { is_expected.not_to be_valid }
223
228
  end
224
229
 
225
230
  context 'when file size within the specified size' do
226
- before { subject.avatar = "{\"filename\":\"img140910_88338.GIF\",\"content_type\":\"image/gif\",\"size\":33150}" }
231
+ before do
232
+ subject.avatar = '{"filename":"img140910_88338.GIF","content_type":"image/gif","size":33150}'
233
+ end
234
+
227
235
  it { is_expected.to be_valid }
228
236
  end
229
237
 
230
238
  context 'empty json string' do
231
- before { subject.avatar = "{}" }
239
+ before { subject.avatar = '{}' }
232
240
  it { is_expected.to be_valid }
233
241
  end
234
242
 
235
- context 'empty json string' do
236
- before { subject.avatar = "" }
243
+ context 'empty string' do
244
+ before { subject.avatar = '' }
237
245
  it { is_expected.to be_valid }
238
246
  end
247
+
248
+ context 'invalid json string' do
249
+ before { subject.avatar = '{filename":"img140910_88338.GIF","content_type":"image/gif","size":33150}' }
250
+ it { is_expected.not_to be_valid }
251
+ end
239
252
  end
240
253
 
241
254
  context 'image data as hash' do
@@ -249,12 +262,26 @@ describe 'File Size Validator integration with ActiveModel' do
249
262
  subject { Person.new }
250
263
 
251
264
  context 'when file size is less than the specified size' do
252
- before { subject.avatar = { "filename" => "img140910_88338.GIF", "content_type" => "image/gif", "size" => 13150 } }
265
+ before do
266
+ subject.avatar = {
267
+ 'filename' => 'img140910_88338.GIF',
268
+ 'content_type' => 'image/gif',
269
+ 'size' => 13_150
270
+ }
271
+ end
272
+
253
273
  it { is_expected.not_to be_valid }
254
274
  end
255
275
 
256
276
  context 'when file size within the specified size' do
257
- before { subject.avatar = { "filename" => "img140910_88338.GIF", "content_type" => "image/gif", "size" => 33150 } }
277
+ before do
278
+ subject.avatar = {
279
+ 'filename' => 'img140910_88338.GIF',
280
+ 'content_type' => 'image/gif',
281
+ 'size' => 33_150
282
+ }
283
+ end
284
+
258
285
  it { is_expected.to be_valid }
259
286
  end
260
287
 
@@ -263,4 +290,67 @@ describe 'File Size Validator integration with ActiveModel' do
263
290
  it { is_expected.to be_valid }
264
291
  end
265
292
  end
293
+
294
+ context 'image data as array' do
295
+ before :all do
296
+ Person.class_eval do
297
+ Person.reset_callbacks(:validate)
298
+ validates :avatar, file_size: { greater_than: 20.kilobytes }
299
+ end
300
+ end
301
+
302
+ subject { Person.new }
303
+
304
+ context 'when size of one file is less than the specified size' do
305
+ before do
306
+ subject.avatar = [
307
+ Rack::Test::UploadedFile.new(@cute_path),
308
+ Rack::Test::UploadedFile.new(@chubby_bubble_path)
309
+ ]
310
+ end
311
+ it { is_expected.not_to be_valid }
312
+ end
313
+
314
+ context 'when size of all files is within the specified size' do
315
+ before do
316
+ subject.avatar = [
317
+ Rack::Test::UploadedFile.new(@cute_path),
318
+ Rack::Test::UploadedFile.new(@cute_path)
319
+ ]
320
+ end
321
+
322
+ it 'is invalid and adds just one error' do
323
+ expect(subject).not_to be_valid
324
+ expect(subject.errors.count).to eq 1
325
+ end
326
+ end
327
+
328
+ context 'when size of all files is less than the specified size' do
329
+ before do
330
+ subject.avatar = [
331
+ Rack::Test::UploadedFile.new(@chubby_bubble_path),
332
+ Rack::Test::UploadedFile.new(@chubby_bubble_path)
333
+ ]
334
+ end
335
+
336
+ it { is_expected.to be_valid }
337
+ end
338
+
339
+ context 'one file' do
340
+ context 'when file size is out of range' do
341
+ before { subject.avatar = [Rack::Test::UploadedFile.new(@cute_path)] }
342
+ it { is_expected.not_to be_valid }
343
+ end
344
+
345
+ context 'when file size within range' do
346
+ before { subject.avatar = [Rack::Test::UploadedFile.new(@chubby_bubble_path)] }
347
+ it { is_expected.to be_valid }
348
+ end
349
+ end
350
+
351
+ context 'empty array' do
352
+ before { subject.avatar = [] }
353
+ it { is_expected.to be_valid }
354
+ end
355
+ end
266
356
  end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'rack/test/uploaded_file'
5
+
6
+ describe FileValidators::MimeTypeAnalyzer do
7
+ it 'rises error when tool is invalid' do
8
+ expect { described_class.new(:invalid) }.to raise_error(FileValidators::Error)
9
+ end
10
+
11
+ before :all do
12
+ @cute_path = File.join(File.dirname(__FILE__), '../../fixtures/cute.jpg')
13
+ @spoofed_file_path = File.join(File.dirname(__FILE__), '../../fixtures/spoofed.jpg')
14
+ end
15
+
16
+ let(:cute_image) { Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg') }
17
+ let(:spoofed_file) { Rack::Test::UploadedFile.new(@spoofed_file_path, 'image/jpeg') }
18
+
19
+ describe ':file analyzer' do
20
+ let(:analyzer) { described_class.new(:file) }
21
+
22
+ it 'determines MIME type from file contents' do
23
+ expect(analyzer.call(cute_image)).to eq('image/jpeg')
24
+ end
25
+
26
+ it 'returns text/plain for unidentified MIME types' do
27
+ expect(analyzer.call(fakeio('a' * 5 * 1024 * 1024))).to eq('text/plain')
28
+ end
29
+
30
+ it 'is able to determine MIME type for spoofed files' do
31
+ expect(analyzer.call(spoofed_file)).to eq('text/plain')
32
+ end
33
+
34
+ it 'is able to determine MIME type for non-files' do
35
+ expect(analyzer.call(fakeio(cute_image.read))).to eq('image/jpeg')
36
+ end
37
+
38
+ it 'returns nil for empty IOs' do
39
+ expect(analyzer.call(fakeio(''))).to eq(nil)
40
+ end
41
+
42
+ it 'raises error if file command is not found' do
43
+ allow(Open3).to receive(:popen3).and_raise(Errno::ENOENT)
44
+ expect { analyzer.call(fakeio) }.to raise_error(FileValidators::Error, 'file command-line tool is not installed')
45
+ end
46
+ end
47
+
48
+ describe ':fastimage analyzer' do
49
+ let(:analyzer) { described_class.new(:fastimage) }
50
+
51
+ it 'extracts MIME type of any IO' do
52
+ expect(analyzer.call(cute_image)).to eq('image/jpeg')
53
+ end
54
+
55
+ it 'returns nil for unidentified MIME types' do
56
+ expect(analyzer.call(fakeio('😃'))).to eq nil
57
+ end
58
+
59
+ it 'returns nil for empty IOs' do
60
+ expect(analyzer.call(fakeio(''))).to eq nil
61
+ end
62
+ end
63
+
64
+ describe ':mimemagic analyzer' do
65
+ let(:analyzer) { described_class.new(:mimemagic) }
66
+
67
+ it 'extracts MIME type of any IO' do
68
+ expect(analyzer.call(cute_image)).to eq('image/jpeg')
69
+ end
70
+
71
+ it 'returns nil for unidentified MIME types' do
72
+ expect(analyzer.call(fakeio('😃'))).to eq nil
73
+ end
74
+
75
+ it 'returns nil for empty IOs' do
76
+ expect(analyzer.call(fakeio(''))).to eq nil
77
+ end
78
+ end
79
+
80
+ if RUBY_VERSION >= '2.2.0'
81
+ describe ':marcel analyzer' do
82
+ let(:analyzer) { described_class.new(:marcel) }
83
+
84
+ it 'extracts MIME type of any IO' do
85
+ expect(analyzer.call(cute_image)).to eq('image/jpeg')
86
+ end
87
+
88
+ it 'returns application/octet-stream for unidentified MIME types' do
89
+ expect(analyzer.call(fakeio('😃'))).to eq 'application/octet-stream'
90
+ end
91
+
92
+ it 'returns nil for empty IOs' do
93
+ expect(analyzer.call(fakeio(''))).to eq nil
94
+ end
95
+ end
96
+ end
97
+
98
+ describe ':mime_types analyzer' do
99
+ let(:analyzer) { described_class.new(:mime_types) }
100
+
101
+ it 'extract MIME type from the file extension' do
102
+ expect(analyzer.call(fakeio(filename: 'image.png'))).to eq('image/png')
103
+ expect(analyzer.call(cute_image)).to eq('image/jpeg')
104
+ end
105
+
106
+ it 'extracts MIME type from file extension when IO is empty' do
107
+ expect(analyzer.call(fakeio('', filename: 'image.png'))).to eq('image/png')
108
+ end
109
+
110
+ it 'returns nil on unknown extension' do
111
+ expect(analyzer.call(fakeio(filename: 'file.foo'))).to eq(nil)
112
+ end
113
+
114
+ it 'returns nil when input is not a file' do
115
+ expect(analyzer.call(fakeio)).to eq(nil)
116
+ end
117
+ end
118
+
119
+ describe ':mini_mime analyzer' do
120
+ let(:analyzer) { described_class.new(:mini_mime) }
121
+
122
+ it 'extract MIME type from the file extension' do
123
+ expect(analyzer.call(fakeio(filename: 'image.png'))).to eq('image/png')
124
+ expect(analyzer.call(cute_image)).to eq('image/jpeg')
125
+ end
126
+
127
+ it 'extracts MIME type from file extension when IO is empty' do
128
+ expect(analyzer.call(fakeio('', filename: 'image.png'))).to eq('image/png')
129
+ end
130
+
131
+ it 'returns nil on unkown extension' do
132
+ expect(analyzer.call(fakeio(filename: 'file.foo'))).to eq(nil)
133
+ end
134
+
135
+ it 'returns nil when input is not a file' do
136
+ expect(analyzer.call(fakeio)).to eq(nil)
137
+ end
138
+ end
139
+ end
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
- require 'file_validators/validators/file_content_type_validator'
3
4
 
4
5
  describe ActiveModel::Validations::FileContentTypeValidator do
5
6
  class Dummy
@@ -9,7 +10,7 @@ describe ActiveModel::Validations::FileContentTypeValidator do
9
10
  subject { Dummy }
10
11
 
11
12
  def build_validator(options)
12
- @validator = ActiveModel::Validations::FileContentTypeValidator.new(options.merge(attributes: :avatar))
13
+ @validator = described_class.new(options.merge(attributes: :avatar))
13
14
  end
14
15
 
15
16
  context 'whitelist format' do
@@ -18,57 +19,85 @@ describe ActiveModel::Validations::FileContentTypeValidator do
18
19
  before { build_validator allow: 'image/jpg' }
19
20
  it { is_expected.to allow_file_content_type('image/jpg', @validator) }
20
21
  end
21
-
22
+
22
23
  context 'as an regexp' do
23
24
  before { build_validator allow: /^image\/.*/ }
24
25
  it { is_expected.to allow_file_content_type('image/png', @validator) }
25
26
  end
26
-
27
+
27
28
  context 'as a list' do
28
29
  before { build_validator allow: ['image/png', 'image/jpg', 'image/jpeg'] }
29
30
  it { is_expected.to allow_file_content_type('image/png', @validator) }
30
31
  end
31
32
 
32
33
  context 'as a proc' do
33
- before { build_validator allow: lambda { |record| ['image/png', 'image/jpg', 'image/jpeg'] } }
34
+ before { build_validator allow: ->(_record) { ['image/png', 'image/jpg', 'image/jpeg'] } }
34
35
  it { is_expected.to allow_file_content_type('image/png', @validator) }
35
36
  end
36
37
  end
37
-
38
+
38
39
  context 'with a disallowed type' do
39
40
  context 'as a string' do
40
41
  before { build_validator allow: 'image/png' }
41
42
  it { is_expected.not_to allow_file_content_type('image/jpeg', @validator) }
42
43
  end
43
-
44
+
44
45
  context 'as a regexp' do
45
46
  before { build_validator allow: /^text\/.*/ }
46
47
  it { is_expected.not_to allow_file_content_type('image/png', @validator) }
47
48
  end
48
49
 
49
50
  context 'as a proc' do
50
- before { build_validator allow: lambda { |record| /^text\/.*/ } }
51
+ before { build_validator allow: ->(_record) { /^text\/.*/ } }
51
52
  it { is_expected.not_to allow_file_content_type('image/png', @validator) }
52
53
  end
53
-
54
+
54
55
  context 'with :message option' do
55
56
  context 'without interpolation' do
56
- before { build_validator allow: 'image/png', message: 'should be a PNG image' }
57
- it { is_expected.not_to allow_file_content_type('image/jpeg', @validator, message: 'Avatar should be a PNG image') }
57
+ before do
58
+ build_validator allow: 'image/png',
59
+ message: 'should be a PNG image'
60
+ end
61
+
62
+ it do
63
+ is_expected.not_to allow_file_content_type(
64
+ 'image/jpeg', @validator,
65
+ message: 'Avatar should be a PNG image'
66
+ )
67
+ end
58
68
  end
59
-
69
+
60
70
  context 'with interpolation' do
61
- before { build_validator allow: 'image/png', message: 'should have content type %{types}' }
62
- it { is_expected.not_to allow_file_content_type('image/jpeg', @validator,
63
- message: 'Avatar should have content type image/png') }
64
- it { is_expected.to allow_file_content_type('image/png', @validator,
65
- message: 'Avatar should have content type image/png') }
71
+ before do
72
+ build_validator allow: 'image/png',
73
+ message: 'should have content type %{types}'
74
+ end
75
+
76
+ it do
77
+ is_expected.not_to allow_file_content_type(
78
+ 'image/jpeg', @validator,
79
+ message: 'Avatar should have content type image/png'
80
+ )
81
+ end
82
+
83
+ it do
84
+ is_expected.to allow_file_content_type(
85
+ 'image/png', @validator,
86
+ message: 'Avatar should have content type image/png'
87
+ )
88
+ end
66
89
  end
67
90
  end
68
91
 
69
92
  context 'default message' do
70
93
  before { build_validator allow: 'image/png' }
71
- it { is_expected.not_to allow_file_content_type('image/jpeg', @validator, message: 'Avatar file should be one of image/png') }
94
+
95
+ it do
96
+ is_expected.not_to allow_file_content_type(
97
+ 'image/jpeg', @validator,
98
+ message: 'Avatar file should be one of image/png'
99
+ )
100
+ end
72
101
  end
73
102
  end
74
103
  end
@@ -79,23 +108,23 @@ describe ActiveModel::Validations::FileContentTypeValidator do
79
108
  before { build_validator exclude: 'image/gif' }
80
109
  it { is_expected.to allow_file_content_type('image/png', @validator) }
81
110
  end
82
-
111
+
83
112
  context 'as an regexp' do
84
113
  before { build_validator exclude: /^text\/.*/ }
85
114
  it { is_expected.to allow_file_content_type('image/png', @validator) }
86
115
  end
87
-
116
+
88
117
  context 'as a list' do
89
118
  before { build_validator exclude: ['image/png', 'image/jpg', 'image/jpeg'] }
90
119
  it { is_expected.to allow_file_content_type('image/gif', @validator) }
91
120
  end
92
121
 
93
122
  context 'as a proc' do
94
- before { build_validator exclude: lambda { |record| ['image/png', 'image/jpg', 'image/jpeg'] } }
123
+ before { build_validator exclude: ->(_record) { ['image/png', 'image/jpg', 'image/jpeg'] } }
95
124
  it { is_expected.to allow_file_content_type('image/gif', @validator) }
96
125
  end
97
126
  end
98
-
127
+
99
128
  context 'with a disallowed type' do
100
129
  context 'as a string' do
101
130
  before { build_validator exclude: 'image/gif' }
@@ -108,28 +137,56 @@ describe ActiveModel::Validations::FileContentTypeValidator do
108
137
  end
109
138
 
110
139
  context 'as an proc' do
111
- before { build_validator exclude: lambda { |record| /^text\/.*/ } }
140
+ before { build_validator exclude: ->(_record) { /^text\/.*/ } }
112
141
  it { is_expected.not_to allow_file_content_type('text/plain', @validator) }
113
142
  end
114
-
143
+
115
144
  context 'with :message option' do
116
145
  context 'without interpolation' do
117
- before { build_validator exclude: 'image/png', message: 'should not be a PNG image' }
118
- it { is_expected.not_to allow_file_content_type('image/png', @validator, message: 'Avatar should not be a PNG image') }
146
+ before do
147
+ build_validator exclude: 'image/png',
148
+ message: 'should not be a PNG image'
149
+ end
150
+
151
+ it do
152
+ is_expected.not_to allow_file_content_type(
153
+ 'image/png', @validator,
154
+ message: 'Avatar should not be a PNG image'
155
+ )
156
+ end
119
157
  end
120
-
158
+
121
159
  context 'with interpolation' do
122
- before { build_validator exclude: 'image/png', message: 'should not have content type %{types}' }
123
- it { is_expected.not_to allow_file_content_type('image/png', @validator,
124
- message: 'Avatar should not have content type image/png') }
125
- it { is_expected.to allow_file_content_type('image/jpeg', @validator,
126
- message: 'Avatar should not have content type image/jpeg') }
160
+ before do
161
+ build_validator exclude: 'image/png',
162
+ message: 'should not have content type %{types}'
163
+ end
164
+
165
+ it do
166
+ is_expected.not_to allow_file_content_type(
167
+ 'image/png', @validator,
168
+ message: 'Avatar should not have content type image/png'
169
+ )
170
+ end
171
+
172
+ it do
173
+ is_expected.to allow_file_content_type(
174
+ 'image/jpeg', @validator,
175
+ message: 'Avatar should not have content type image/jpeg'
176
+ )
177
+ end
127
178
  end
128
179
  end
129
180
 
130
181
  context 'default message' do
131
182
  before { build_validator exclude: 'image/png' }
132
- it { is_expected.not_to allow_file_content_type('image/png', @validator, message: 'Avatar file cannot be image/png') }
183
+
184
+ it do
185
+ is_expected.not_to allow_file_content_type(
186
+ 'image/png', @validator,
187
+ message: 'Avatar file cannot be image/png'
188
+ )
189
+ end
133
190
  end
134
191
  end
135
192
  end
@@ -138,7 +195,7 @@ describe ActiveModel::Validations::FileContentTypeValidator do
138
195
  before { Dummy.validates_file_content_type :avatar, allow: 'image/jpg' }
139
196
 
140
197
  it 'adds the validator to the class' do
141
- expect(Dummy.validators_on(:avatar)).to include(ActiveModel::Validations::FileContentTypeValidator)
198
+ expect(Dummy.validators_on(:avatar)).to include(described_class)
142
199
  end
143
200
  end
144
201
 
@@ -147,12 +204,12 @@ describe ActiveModel::Validations::FileContentTypeValidator do
147
204
  expect { build_validator message: 'Some message' }.to raise_error(ArgumentError)
148
205
  end
149
206
 
150
- ActiveModel::Validations::FileContentTypeValidator::CHECKS.each do |argument|
207
+ described_class::CHECKS.each do |argument|
151
208
  it "does not raise error if :#{argument} is string, array, regexp or a proc" do
152
209
  expect { build_validator argument => 'image/jpg' }.not_to raise_error
153
210
  expect { build_validator argument => ['image/jpg'] }.not_to raise_error
154
211
  expect { build_validator argument => /^image\/.*/ }.not_to raise_error
155
- expect { build_validator argument => lambda { |record| 'image/jpg' } }.not_to raise_error
212
+ expect { build_validator argument => ->(_record) { 'image/jpg' } }.not_to raise_error
156
213
  end
157
214
 
158
215
  it "raises argument error if :#{argument} is neither a string, array, regexp nor proc" do