file_validators 2.3.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.
- checksums.yaml +5 -5
- data/.rubocop.yml +32 -0
- data/.tool-versions +1 -0
- data/.travis.yml +36 -6
- data/Appraisals +8 -8
- data/CHANGELOG.md +20 -0
- data/Gemfile +2 -0
- data/README.md +21 -16
- data/Rakefile +3 -1
- data/file_validators.gemspec +12 -6
- data/gemfiles/activemodel_3.2.gemfile +0 -0
- data/gemfiles/activemodel_4.0.gemfile +0 -0
- data/gemfiles/activemodel_5.0.gemfile +0 -0
- data/gemfiles/{activemodel_4.1.gemfile → activemodel_6.0.gemfile} +1 -2
- data/gemfiles/{activemodel_4.2.gemfile → activemodel_6.1.gemfile} +1 -2
- data/lib/file_validators/error.rb +6 -0
- data/lib/file_validators/mime_type_analyzer.rb +106 -0
- data/lib/file_validators/validators/file_content_type_validator.rb +33 -42
- data/lib/file_validators/validators/file_size_validator.rb +43 -19
- data/lib/file_validators/version.rb +3 -1
- data/lib/file_validators.rb +6 -7
- data/spec/integration/combined_validators_integration_spec.rb +3 -1
- data/spec/integration/file_content_type_validation_integration_spec.rb +73 -17
- data/spec/integration/file_size_validator_integration_spec.rb +43 -16
- data/spec/lib/file_validators/mime_type_analyzer_spec.rb +139 -0
- data/spec/lib/file_validators/validators/file_content_type_validator_spec.rb +90 -32
- data/spec/lib/file_validators/validators/file_size_validator_spec.rb +78 -30
- data/spec/spec_helper.rb +4 -0
- data/spec/support/fakeio.rb +17 -0
- data/spec/support/helpers.rb +7 -0
- data/spec/support/matchers/allow_content_type.rb +2 -0
- data/spec/support/matchers/allow_file_size.rb +2 -0
- metadata +82 -23
- data/lib/file_validators/utils/content_type_detector.rb +0 -67
- data/lib/file_validators/utils/media_type_spoof_detector.rb +0 -46
- data/spec/lib/file_validators/utils/content_type_detector_spec.rb +0 -27
- data/spec/lib/file_validators/utils/media_type_spoof_detector_spec.rb +0 -31
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ActiveModel
|
|
2
4
|
module Validations
|
|
3
|
-
|
|
4
5
|
class FileSizeValidator < ActiveModel::EachValidator
|
|
5
6
|
CHECKS = { in: :===,
|
|
6
7
|
less_than: :<,
|
|
@@ -13,23 +14,25 @@ module ActiveModel
|
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def validate_each(record, attribute, value)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
begin
|
|
18
|
+
values = parse_values(value)
|
|
19
|
+
rescue JSON::ParserError
|
|
20
|
+
record.errors.add attribute, :invalid
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
return if values.empty?
|
|
25
|
+
|
|
26
|
+
options.slice(*CHECKS.keys).each do |option, option_value|
|
|
27
|
+
check_errors(record, attribute, values, option, option_value)
|
|
26
28
|
end
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
def check_validity!
|
|
30
32
|
unless (CHECKS.keys & options.keys).present?
|
|
31
|
-
raise ArgumentError, 'You must at least pass in one of these options
|
|
32
|
-
|
|
33
|
+
raise ArgumentError, 'You must at least pass in one of these options' \
|
|
34
|
+
' - :in, :less_than, :less_than_or_equal_to,' \
|
|
35
|
+
' :greater_than and :greater_than_or_equal_to'
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
check_options(Numeric, options.slice(*(CHECKS.keys - [:in])))
|
|
@@ -46,7 +49,7 @@ module ActiveModel
|
|
|
46
49
|
|
|
47
50
|
value = OpenStruct.new(value) if value.is_a?(Hash)
|
|
48
51
|
|
|
49
|
-
Array.wrap(value).reject
|
|
52
|
+
Array.wrap(value).reject(&:blank?)
|
|
50
53
|
end
|
|
51
54
|
|
|
52
55
|
def check_options(klass, options)
|
|
@@ -57,6 +60,18 @@ module ActiveModel
|
|
|
57
60
|
end
|
|
58
61
|
end
|
|
59
62
|
|
|
63
|
+
def check_errors(record, attribute, values, option, option_value)
|
|
64
|
+
option_value = option_value.call(record) if option_value.is_a?(Proc)
|
|
65
|
+
has_invalid_size = values.any? { |v| !valid_size?(value_byte_size(v), option, option_value) }
|
|
66
|
+
if has_invalid_size
|
|
67
|
+
record.errors.add(
|
|
68
|
+
attribute,
|
|
69
|
+
"file_size_is_#{option}".to_sym,
|
|
70
|
+
**filtered_options(values).merge!(detect_error_options(option_value))
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
60
75
|
def value_byte_size(value)
|
|
61
76
|
if value.respond_to?(:byte_size)
|
|
62
77
|
value.byte_size
|
|
@@ -82,7 +97,7 @@ module ActiveModel
|
|
|
82
97
|
|
|
83
98
|
def detect_error_options(option_value)
|
|
84
99
|
if option_value.is_a?(Range)
|
|
85
|
-
{ min: human_size(option_value.min), max: human_size(option_value.max)
|
|
100
|
+
{ min: human_size(option_value.min), max: human_size(option_value.max) }
|
|
86
101
|
else
|
|
87
102
|
{ count: human_size(option_value) }
|
|
88
103
|
end
|
|
@@ -92,12 +107,22 @@ module ActiveModel
|
|
|
92
107
|
if defined?(ActiveSupport::NumberHelper) # Rails 4.0+
|
|
93
108
|
ActiveSupport::NumberHelper.number_to_human_size(size)
|
|
94
109
|
else
|
|
95
|
-
storage_units_format = I18n.translate(
|
|
96
|
-
|
|
110
|
+
storage_units_format = I18n.translate(
|
|
111
|
+
:'number.human.storage_units.format',
|
|
112
|
+
locale: options[:locale],
|
|
113
|
+
raise: true
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
unit = I18n.translate(
|
|
117
|
+
:'number.human.storage_units.units.byte',
|
|
118
|
+
locale: options[:locale],
|
|
119
|
+
count: size.to_i,
|
|
120
|
+
raise: true
|
|
121
|
+
)
|
|
122
|
+
|
|
97
123
|
storage_units_format.gsub(/%n/, size.to_i.to_s).gsub(/%u/, unit).html_safe
|
|
98
124
|
end
|
|
99
125
|
end
|
|
100
|
-
|
|
101
126
|
end
|
|
102
127
|
|
|
103
128
|
module HelperMethods
|
|
@@ -116,6 +141,5 @@ module ActiveModel
|
|
|
116
141
|
validates_with FileSizeValidator, _merge_attributes(attr_names)
|
|
117
142
|
end
|
|
118
143
|
end
|
|
119
|
-
|
|
120
144
|
end
|
|
121
145
|
end
|
data/lib/file_validators.rb
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'active_model'
|
|
2
4
|
require 'ostruct'
|
|
3
5
|
|
|
4
6
|
module FileValidators
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
autoload :ContentTypeDetector
|
|
9
|
-
autoload :MediaTypeSpoofDetector
|
|
10
|
-
end
|
|
7
|
+
extend ActiveSupport::Autoload
|
|
8
|
+
autoload :Error
|
|
9
|
+
autoload :MimeTypeAnalyzer
|
|
11
10
|
end
|
|
12
11
|
|
|
13
|
-
Dir[File.dirname(__FILE__) +
|
|
12
|
+
Dir[File.dirname(__FILE__) + '/file_validators/validators/*.rb'].each { |file| require file }
|
|
14
13
|
|
|
15
14
|
locale_path = Dir.glob(File.dirname(__FILE__) + '/file_validators/locale/*.yml')
|
|
16
15
|
I18n.load_path += locale_path unless I18n.load_path.include?(locale_path)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'spec_helper'
|
|
2
4
|
require 'rack/test/uploaded_file'
|
|
3
5
|
|
|
@@ -52,7 +54,7 @@ describe 'Combined File Validators integration with ActiveModel' do
|
|
|
52
54
|
before :all do
|
|
53
55
|
Person.class_eval do
|
|
54
56
|
Person.reset_callbacks(:validate)
|
|
55
|
-
validates_file_size :avatar,
|
|
57
|
+
validates_file_size :avatar, less_than: 20.kilobytes
|
|
56
58
|
validates_file_content_type :avatar, allow: 'image/jpeg'
|
|
57
59
|
end
|
|
58
60
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'spec_helper'
|
|
2
4
|
require 'rack/test/uploaded_file'
|
|
3
5
|
|
|
@@ -69,7 +71,8 @@ describe 'File Content Type integration with ActiveModel' do
|
|
|
69
71
|
before :all do
|
|
70
72
|
Person.class_eval do
|
|
71
73
|
Person.reset_callbacks(:validate)
|
|
72
|
-
validates :avatar, file_content_type: { allow: ['image/jpeg', 'text/plain'],
|
|
74
|
+
validates :avatar, file_content_type: { allow: ['image/jpeg', 'text/plain'],
|
|
75
|
+
mode: :strict }
|
|
73
76
|
end
|
|
74
77
|
end
|
|
75
78
|
|
|
@@ -97,7 +100,7 @@ describe 'File Content Type integration with ActiveModel' do
|
|
|
97
100
|
before :all do
|
|
98
101
|
Person.class_eval do
|
|
99
102
|
Person.reset_callbacks(:validate)
|
|
100
|
-
validates :avatar, file_content_type: { allow:
|
|
103
|
+
validates :avatar, file_content_type: { allow: ->(_record) { ['image/jpeg', 'text/plain'] },
|
|
101
104
|
mode: :strict }
|
|
102
105
|
end
|
|
103
106
|
end
|
|
@@ -177,7 +180,8 @@ describe 'File Content Type integration with ActiveModel' do
|
|
|
177
180
|
before :all do
|
|
178
181
|
Person.class_eval do
|
|
179
182
|
Person.reset_callbacks(:validate)
|
|
180
|
-
validates :avatar, file_content_type: { exclude: ['image/jpeg', 'text/plain'],
|
|
183
|
+
validates :avatar, file_content_type: { exclude: ['image/jpeg', 'text/plain'],
|
|
184
|
+
mode: :strict }
|
|
181
185
|
end
|
|
182
186
|
end
|
|
183
187
|
|
|
@@ -205,7 +209,8 @@ describe 'File Content Type integration with ActiveModel' do
|
|
|
205
209
|
before :all do
|
|
206
210
|
Person.class_eval do
|
|
207
211
|
Person.reset_callbacks(:validate)
|
|
208
|
-
validates :avatar, file_content_type: { exclude:
|
|
212
|
+
validates :avatar, file_content_type: { exclude: ->(_record) { /^image\/.*/ },
|
|
213
|
+
mode: :strict }
|
|
209
214
|
end
|
|
210
215
|
end
|
|
211
216
|
|
|
@@ -229,7 +234,8 @@ describe 'File Content Type integration with ActiveModel' do
|
|
|
229
234
|
before :all do
|
|
230
235
|
Person.class_eval do
|
|
231
236
|
Person.reset_callbacks(:validate)
|
|
232
|
-
validates :avatar, file_content_type: { allow: /^image\/.*/, exclude: 'image/png',
|
|
237
|
+
validates :avatar, file_content_type: { allow: /^image\/.*/, exclude: 'image/png',
|
|
238
|
+
mode: :strict }
|
|
233
239
|
end
|
|
234
240
|
end
|
|
235
241
|
|
|
@@ -246,6 +252,31 @@ describe 'File Content Type integration with ActiveModel' do
|
|
|
246
252
|
end
|
|
247
253
|
end
|
|
248
254
|
|
|
255
|
+
context ':tool option' do
|
|
256
|
+
before :all do
|
|
257
|
+
Person.class_eval do
|
|
258
|
+
Person.reset_callbacks(:validate)
|
|
259
|
+
validates :avatar, file_content_type: { allow: 'image/jpeg', tool: :marcel }
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
subject { Person.new }
|
|
264
|
+
|
|
265
|
+
context 'with valid file' do
|
|
266
|
+
it 'validates the file' do
|
|
267
|
+
subject.avatar = Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg')
|
|
268
|
+
expect(subject).to be_valid
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
context 'with spoofed file' do
|
|
273
|
+
it 'invalidates the file' do
|
|
274
|
+
subject.avatar = Rack::Test::UploadedFile.new(@spoofed_file_path, 'image/jpeg')
|
|
275
|
+
expect(subject).not_to be_valid
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
249
280
|
context ':mode option' do
|
|
250
281
|
context 'strict mode' do
|
|
251
282
|
before :all do
|
|
@@ -334,24 +365,35 @@ describe 'File Content Type integration with ActiveModel' do
|
|
|
334
365
|
subject { Person.new }
|
|
335
366
|
|
|
336
367
|
context 'for invalid content type' do
|
|
337
|
-
before
|
|
368
|
+
before do
|
|
369
|
+
subject.avatar = '{"filename":"img140910_88338.GIF","content_type":"image/gif","size":13150}'
|
|
370
|
+
end
|
|
371
|
+
|
|
338
372
|
it { is_expected.not_to be_valid }
|
|
339
373
|
end
|
|
340
374
|
|
|
341
375
|
context 'for valid content type' do
|
|
342
|
-
before
|
|
376
|
+
before do
|
|
377
|
+
subject.avatar = '{"filename":"img140910_88338.jpg","content_type":"image/jpeg","size":13150}'
|
|
378
|
+
end
|
|
379
|
+
|
|
343
380
|
it { is_expected.to be_valid }
|
|
344
381
|
end
|
|
345
382
|
|
|
346
383
|
context 'empty json string' do
|
|
347
|
-
before { subject.avatar =
|
|
384
|
+
before { subject.avatar = '{}' }
|
|
348
385
|
it { is_expected.to be_valid }
|
|
349
386
|
end
|
|
350
387
|
|
|
351
388
|
context 'empty string' do
|
|
352
|
-
before { subject.avatar =
|
|
389
|
+
before { subject.avatar = '' }
|
|
353
390
|
it { is_expected.to be_valid }
|
|
354
391
|
end
|
|
392
|
+
|
|
393
|
+
context 'invalid json string' do
|
|
394
|
+
before { subject.avatar = '{filename":"img140910_88338.jpg","content_type":"image/jpeg","size":13150}' }
|
|
395
|
+
it { is_expected.not_to be_valid }
|
|
396
|
+
end
|
|
355
397
|
end
|
|
356
398
|
|
|
357
399
|
context 'image data as hash' do
|
|
@@ -365,12 +407,26 @@ describe 'File Content Type integration with ActiveModel' do
|
|
|
365
407
|
subject { Person.new }
|
|
366
408
|
|
|
367
409
|
context 'for invalid content type' do
|
|
368
|
-
before
|
|
410
|
+
before do
|
|
411
|
+
subject.avatar = {
|
|
412
|
+
'filename' => 'img140910_88338.GIF',
|
|
413
|
+
'content_type' => 'image/gif',
|
|
414
|
+
'size' => 13_150
|
|
415
|
+
}
|
|
416
|
+
end
|
|
417
|
+
|
|
369
418
|
it { is_expected.not_to be_valid }
|
|
370
419
|
end
|
|
371
420
|
|
|
372
421
|
context 'for valid content type' do
|
|
373
|
-
before
|
|
422
|
+
before do
|
|
423
|
+
subject.avatar = {
|
|
424
|
+
'filename' => 'img140910_88338.jpg',
|
|
425
|
+
'content_type' => 'image/jpeg',
|
|
426
|
+
'size' => 13_150
|
|
427
|
+
}
|
|
428
|
+
end
|
|
429
|
+
|
|
374
430
|
it { is_expected.to be_valid }
|
|
375
431
|
end
|
|
376
432
|
|
|
@@ -391,22 +447,22 @@ describe 'File Content Type integration with ActiveModel' do
|
|
|
391
447
|
subject { Person.new }
|
|
392
448
|
|
|
393
449
|
context 'for one invalid content type' do
|
|
394
|
-
before
|
|
450
|
+
before do
|
|
395
451
|
subject.avatar = [
|
|
396
452
|
Rack::Test::UploadedFile.new(@sample_text_path, 'text/plain'),
|
|
397
453
|
Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg')
|
|
398
454
|
]
|
|
399
|
-
|
|
455
|
+
end
|
|
400
456
|
it { is_expected.not_to be_valid }
|
|
401
457
|
end
|
|
402
458
|
|
|
403
459
|
context 'for two invalid content types' do
|
|
404
|
-
before
|
|
460
|
+
before do
|
|
405
461
|
subject.avatar = [
|
|
406
462
|
Rack::Test::UploadedFile.new(@sample_text_path, 'text/plain'),
|
|
407
463
|
Rack::Test::UploadedFile.new(@sample_text_path, 'text/plain')
|
|
408
464
|
]
|
|
409
|
-
|
|
465
|
+
end
|
|
410
466
|
|
|
411
467
|
it 'is invalid and adds just one error' do
|
|
412
468
|
expect(subject).not_to be_valid
|
|
@@ -415,12 +471,12 @@ describe 'File Content Type integration with ActiveModel' do
|
|
|
415
471
|
end
|
|
416
472
|
|
|
417
473
|
context 'for valid content type' do
|
|
418
|
-
before
|
|
474
|
+
before do
|
|
419
475
|
subject.avatar = [
|
|
420
476
|
Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg'),
|
|
421
477
|
Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg')
|
|
422
478
|
]
|
|
423
|
-
|
|
479
|
+
end
|
|
424
480
|
it { is_expected.to be_valid }
|
|
425
481
|
end
|
|
426
482
|
|
|
@@ -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:
|
|
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:
|
|
103
|
-
less_than:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
|
@@ -275,22 +302,22 @@ describe 'File Size Validator integration with ActiveModel' do
|
|
|
275
302
|
subject { Person.new }
|
|
276
303
|
|
|
277
304
|
context 'when size of one file is less than the specified size' do
|
|
278
|
-
before
|
|
305
|
+
before do
|
|
279
306
|
subject.avatar = [
|
|
280
307
|
Rack::Test::UploadedFile.new(@cute_path),
|
|
281
308
|
Rack::Test::UploadedFile.new(@chubby_bubble_path)
|
|
282
309
|
]
|
|
283
|
-
|
|
310
|
+
end
|
|
284
311
|
it { is_expected.not_to be_valid }
|
|
285
312
|
end
|
|
286
313
|
|
|
287
314
|
context 'when size of all files is within the specified size' do
|
|
288
|
-
before
|
|
315
|
+
before do
|
|
289
316
|
subject.avatar = [
|
|
290
317
|
Rack::Test::UploadedFile.new(@cute_path),
|
|
291
318
|
Rack::Test::UploadedFile.new(@cute_path)
|
|
292
319
|
]
|
|
293
|
-
|
|
320
|
+
end
|
|
294
321
|
|
|
295
322
|
it 'is invalid and adds just one error' do
|
|
296
323
|
expect(subject).not_to be_valid
|
|
@@ -299,12 +326,12 @@ describe 'File Size Validator integration with ActiveModel' do
|
|
|
299
326
|
end
|
|
300
327
|
|
|
301
328
|
context 'when size of all files is less than the specified size' do
|
|
302
|
-
before
|
|
329
|
+
before do
|
|
303
330
|
subject.avatar = [
|
|
304
331
|
Rack::Test::UploadedFile.new(@chubby_bubble_path),
|
|
305
332
|
Rack::Test::UploadedFile.new(@chubby_bubble_path)
|
|
306
333
|
]
|
|
307
|
-
|
|
334
|
+
end
|
|
308
335
|
|
|
309
336
|
it { is_expected.to be_valid }
|
|
310
337
|
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
|