file_validators 2.1.0 → 3.0.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 (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