file_validators 2.1.0 → 3.0.0

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 +2 -0
  3. data/.rubocop.yml +32 -0
  4. data/.tool-versions +1 -0
  5. data/.travis.yml +43 -7
  6. data/Appraisals +13 -13
  7. data/CHANGELOG.md +31 -0
  8. data/Gemfile +2 -0
  9. data/README.md +26 -18
  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.2.gemfile → activemodel_5.0.gemfile} +1 -1
  15. data/gemfiles/{activemodel_4.1.gemfile → activemodel_6.0.gemfile} +1 -1
  16. data/gemfiles/{activemodel_3.0.gemfile → activemodel_6.1.gemfile} +1 -1
  17. data/lib/file_validators.rb +6 -7
  18. data/lib/file_validators/error.rb +6 -0
  19. data/lib/file_validators/mime_type_analyzer.rb +106 -0
  20. data/lib/file_validators/validators/file_content_type_validator.rb +37 -33
  21. data/lib/file_validators/validators/file_size_validator.rb +62 -19
  22. data/lib/file_validators/version.rb +3 -1
  23. data/spec/integration/combined_validators_integration_spec.rb +3 -1
  24. data/spec/integration/file_content_type_validation_integration_spec.rb +117 -11
  25. data/spec/integration/file_size_validator_integration_spec.rb +100 -10
  26. data/spec/lib/file_validators/mime_type_analyzer_spec.rb +139 -0
  27. data/spec/lib/file_validators/validators/file_content_type_validator_spec.rb +90 -32
  28. data/spec/lib/file_validators/validators/file_size_validator_spec.rb +84 -30
  29. data/spec/spec_helper.rb +5 -0
  30. data/spec/support/fakeio.rb +17 -0
  31. data/spec/support/helpers.rb +7 -0
  32. data/spec/support/matchers/allow_content_type.rb +2 -0
  33. data/spec/support/matchers/allow_file_size.rb +2 -0
  34. metadata +86 -28
  35. data/CHANGELOG.mod +0 -6
  36. data/gemfiles/activemodel_3.1.gemfile +0 -8
  37. data/lib/file_validators/utils/content_type_detector.rb +0 -64
  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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe ActiveModel::Validations::FileContentTypeValidator do
@@ -17,57 +19,85 @@ describe ActiveModel::Validations::FileContentTypeValidator do
17
19
  before { build_validator allow: 'image/jpg' }
18
20
  it { is_expected.to allow_file_content_type('image/jpg', @validator) }
19
21
  end
20
-
22
+
21
23
  context 'as an regexp' do
22
24
  before { build_validator allow: /^image\/.*/ }
23
25
  it { is_expected.to allow_file_content_type('image/png', @validator) }
24
26
  end
25
-
27
+
26
28
  context 'as a list' do
27
29
  before { build_validator allow: ['image/png', 'image/jpg', 'image/jpeg'] }
28
30
  it { is_expected.to allow_file_content_type('image/png', @validator) }
29
31
  end
30
32
 
31
33
  context 'as a proc' do
32
- before { build_validator allow: lambda { |record| ['image/png', 'image/jpg', 'image/jpeg'] } }
34
+ before { build_validator allow: ->(_record) { ['image/png', 'image/jpg', 'image/jpeg'] } }
33
35
  it { is_expected.to allow_file_content_type('image/png', @validator) }
34
36
  end
35
37
  end
36
-
38
+
37
39
  context 'with a disallowed type' do
38
40
  context 'as a string' do
39
41
  before { build_validator allow: 'image/png' }
40
42
  it { is_expected.not_to allow_file_content_type('image/jpeg', @validator) }
41
43
  end
42
-
44
+
43
45
  context 'as a regexp' do
44
46
  before { build_validator allow: /^text\/.*/ }
45
47
  it { is_expected.not_to allow_file_content_type('image/png', @validator) }
46
48
  end
47
49
 
48
50
  context 'as a proc' do
49
- before { build_validator allow: lambda { |record| /^text\/.*/ } }
51
+ before { build_validator allow: ->(_record) { /^text\/.*/ } }
50
52
  it { is_expected.not_to allow_file_content_type('image/png', @validator) }
51
53
  end
52
-
54
+
53
55
  context 'with :message option' do
54
56
  context 'without interpolation' do
55
- before { build_validator allow: 'image/png', message: 'should be a PNG image' }
56
- 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
57
68
  end
58
-
69
+
59
70
  context 'with interpolation' do
60
- before { build_validator allow: 'image/png', message: 'should have content type %{types}' }
61
- it { is_expected.not_to allow_file_content_type('image/jpeg', @validator,
62
- message: 'Avatar should have content type image/png') }
63
- it { is_expected.to allow_file_content_type('image/png', @validator,
64
- 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
65
89
  end
66
90
  end
67
91
 
68
92
  context 'default message' do
69
93
  before { build_validator allow: 'image/png' }
70
- 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
71
101
  end
72
102
  end
73
103
  end
@@ -78,23 +108,23 @@ describe ActiveModel::Validations::FileContentTypeValidator do
78
108
  before { build_validator exclude: 'image/gif' }
79
109
  it { is_expected.to allow_file_content_type('image/png', @validator) }
80
110
  end
81
-
111
+
82
112
  context 'as an regexp' do
83
113
  before { build_validator exclude: /^text\/.*/ }
84
114
  it { is_expected.to allow_file_content_type('image/png', @validator) }
85
115
  end
86
-
116
+
87
117
  context 'as a list' do
88
118
  before { build_validator exclude: ['image/png', 'image/jpg', 'image/jpeg'] }
89
119
  it { is_expected.to allow_file_content_type('image/gif', @validator) }
90
120
  end
91
121
 
92
122
  context 'as a proc' do
93
- before { build_validator exclude: lambda { |record| ['image/png', 'image/jpg', 'image/jpeg'] } }
123
+ before { build_validator exclude: ->(_record) { ['image/png', 'image/jpg', 'image/jpeg'] } }
94
124
  it { is_expected.to allow_file_content_type('image/gif', @validator) }
95
125
  end
96
126
  end
97
-
127
+
98
128
  context 'with a disallowed type' do
99
129
  context 'as a string' do
100
130
  before { build_validator exclude: 'image/gif' }
@@ -107,28 +137,56 @@ describe ActiveModel::Validations::FileContentTypeValidator do
107
137
  end
108
138
 
109
139
  context 'as an proc' do
110
- before { build_validator exclude: lambda { |record| /^text\/.*/ } }
140
+ before { build_validator exclude: ->(_record) { /^text\/.*/ } }
111
141
  it { is_expected.not_to allow_file_content_type('text/plain', @validator) }
112
142
  end
113
-
143
+
114
144
  context 'with :message option' do
115
145
  context 'without interpolation' do
116
- before { build_validator exclude: 'image/png', message: 'should not be a PNG image' }
117
- 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
118
157
  end
119
-
158
+
120
159
  context 'with interpolation' do
121
- before { build_validator exclude: 'image/png', message: 'should not have content type %{types}' }
122
- it { is_expected.not_to allow_file_content_type('image/png', @validator,
123
- message: 'Avatar should not have content type image/png') }
124
- it { is_expected.to allow_file_content_type('image/jpeg', @validator,
125
- 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
126
178
  end
127
179
  end
128
180
 
129
181
  context 'default message' do
130
182
  before { build_validator exclude: 'image/png' }
131
- 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
132
190
  end
133
191
  end
134
192
  end
@@ -151,7 +209,7 @@ describe ActiveModel::Validations::FileContentTypeValidator do
151
209
  expect { build_validator argument => 'image/jpg' }.not_to raise_error
152
210
  expect { build_validator argument => ['image/jpg'] }.not_to raise_error
153
211
  expect { build_validator argument => /^image\/.*/ }.not_to raise_error
154
- expect { build_validator argument => lambda { |record| 'image/jpg' } }.not_to raise_error
212
+ expect { build_validator argument => ->(_record) { 'image/jpg' } }.not_to raise_error
155
213
  end
156
214
 
157
215
  it "raises argument error if :#{argument} is neither a string, array, regexp nor proc" do