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.
Files changed (37) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +32 -0
  3. data/.tool-versions +1 -0
  4. data/.travis.yml +36 -6
  5. data/Appraisals +8 -8
  6. data/CHANGELOG.md +20 -0
  7. data/Gemfile +2 -0
  8. data/README.md +21 -16
  9. data/Rakefile +3 -1
  10. data/file_validators.gemspec +12 -6
  11. data/gemfiles/activemodel_3.2.gemfile +0 -0
  12. data/gemfiles/activemodel_4.0.gemfile +0 -0
  13. data/gemfiles/activemodel_5.0.gemfile +0 -0
  14. data/gemfiles/{activemodel_4.1.gemfile → activemodel_6.0.gemfile} +1 -2
  15. data/gemfiles/{activemodel_4.2.gemfile → activemodel_6.1.gemfile} +1 -2
  16. data/lib/file_validators/error.rb +6 -0
  17. data/lib/file_validators/mime_type_analyzer.rb +106 -0
  18. data/lib/file_validators/validators/file_content_type_validator.rb +33 -42
  19. data/lib/file_validators/validators/file_size_validator.rb +43 -19
  20. data/lib/file_validators/version.rb +3 -1
  21. data/lib/file_validators.rb +6 -7
  22. data/spec/integration/combined_validators_integration_spec.rb +3 -1
  23. data/spec/integration/file_content_type_validation_integration_spec.rb +73 -17
  24. data/spec/integration/file_size_validator_integration_spec.rb +43 -16
  25. data/spec/lib/file_validators/mime_type_analyzer_spec.rb +139 -0
  26. data/spec/lib/file_validators/validators/file_content_type_validator_spec.rb +90 -32
  27. data/spec/lib/file_validators/validators/file_size_validator_spec.rb +78 -30
  28. data/spec/spec_helper.rb +4 -0
  29. data/spec/support/fakeio.rb +17 -0
  30. data/spec/support/helpers.rb +7 -0
  31. data/spec/support/matchers/allow_content_type.rb +2 -0
  32. data/spec/support/matchers/allow_file_size.rb +2 -0
  33. metadata +82 -23
  34. data/lib/file_validators/utils/content_type_detector.rb +0 -67
  35. data/lib/file_validators/utils/media_type_spoof_detector.rb +0 -46
  36. data/spec/lib/file_validators/utils/content_type_detector_spec.rb +0 -27
  37. 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
- values = parse_values(value)
17
- unless values.empty?
18
- options.slice(*CHECKS.keys).each do |option, option_value|
19
- option_value = option_value.call(record) if option_value.is_a?(Proc)
20
- if values.any? { |v| not valid_size?(value_byte_size(v), option, option_value) }
21
- record.errors.add(attribute,
22
- "file_size_is_#{option}".to_sym,
23
- filtered_options(values).merge!(detect_error_options(option_value)))
24
- end
25
- end
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 - :in, :less_than,
32
- :less_than_or_equal_to, :greater_than and :greater_than_or_equal_to'
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 { |value| value.blank? }
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(:'number.human.storage_units.format', :locale => options[:locale], :raise => true)
96
- unit = I18n.translate(:'number.human.storage_units.units.byte', :locale => options[:locale], :count => size.to_i, :raise => true)
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FileValidators
2
- VERSION = '2.3.0'
4
+ VERSION = '3.0.0'
3
5
  end
@@ -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
- module Utils
6
- extend ActiveSupport::Autoload
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__) + "/file_validators/validators/*.rb"].each { |file| require 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, { less_than: 20.kilobytes }
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'], mode: :strict }
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: lambda { |record| ['image/jpeg', 'text/plain'] },
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'], mode: :strict }
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: lambda { |record| /^image\/.*/ }, mode: :strict }
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', mode: :strict }
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 { subject.avatar = "{\"filename\":\"img140910_88338.GIF\",\"content_type\":\"image/gif\",\"size\":13150}" }
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 { subject.avatar = "{\"filename\":\"img140910_88338.jpg\",\"content_type\":\"image/jpeg\",\"size\":13150}" }
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 { subject.avatar = { "filename" => "img140910_88338.GIF", "content_type" => "image/gif", "size" => 13150 } }
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 { subject.avatar = { "filename" => "img140910_88338.jpg", "content_type" => "image/jpeg", "size" => 13150 } }
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: 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
 
@@ -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