From fc3ae1343df5adb83a3733958a4436981feb380f Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 29 Sep 2021 23:52:36 +0200
Subject: [PATCH] Switch from unmaintained paperclip to kt-paperclip (#16724)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Switch from unmaintained paperclip to kt-paperclip

* Drop some compatibility monkey-patches not required by kt-paperclip

* Drop media spoof check monkey-patching

It's broken with kt-paperclip and hopefully it won't be needed anymore

* Fix regression introduced by paperclip 6.1.0

* Do not rely on pathname to call FastImage

* Add test for ogg vorbis file with cover art

* Add audio/vorbis to the accepted content-types

This seems erroneous as this would be the content-type for a vorbis stream
without an ogg container, but that's what the `marcel` gem outputs, so…

* Restore missing for_as_default method

* Refactor Attachmentable concern and delay Paperclip's content-type spoof check

Check for content-type spoofing *after* setting the extension ourselves, this
fixes a regression with kt-paperclip's validations being more strict than
paperclip 6.0.0 and rejecting some Pleroma uploads because of unknown
extensions.

* Please CodeClimate

* Add audio/vorbis to the unreliable set

It doesn't correspond to a file format and thus has no extension associated.
---
 Gemfile                                       |   2 +-
 Gemfile.lock                                  |  17 ++---
 app/lib/fast_geometry_parser.rb               |   2 +-
 app/models/account.rb                         |   2 +-
 app/models/concerns/attachmentable.rb         |  61 ++++++++----------
 app/models/custom_emoji.rb                    |   6 +-
 app/models/media_attachment.rb                |   6 +-
 app/models/preview_card.rb                    |   6 +-
 config/application.rb                         |   3 -
 lib/paperclip/attachment_extensions.rb        |  29 +++++++++
 .../media_type_spoof_detector_extensions.rb   |  35 ----------
 lib/paperclip/schema_extensions.rb            |  37 -----------
 lib/paperclip/url_generator_extensions.rb     |  10 ---
 lib/paperclip/validation_extensions.rb        |  58 -----------------
 spec/fixtures/files/boop.ogg                  | Bin 0 -> 11379 bytes
 spec/models/media_attachment_spec.rb          |  24 +++++++
 16 files changed, 99 insertions(+), 199 deletions(-)
 delete mode 100644 lib/paperclip/media_type_spoof_detector_extensions.rb
 delete mode 100644 lib/paperclip/schema_extensions.rb
 delete mode 100644 lib/paperclip/validation_extensions.rb
 create mode 100644 spec/fixtures/files/boop.ogg

diff --git a/Gemfile b/Gemfile
index 2ee2a771dd..d871d5ef05 100644
--- a/Gemfile
+++ b/Gemfile
@@ -20,7 +20,7 @@ gem 'dotenv-rails', '~> 2.7'
 gem 'aws-sdk-s3', '~> 1.103', require: false
 gem 'fog-core', '<= 2.1.0'
 gem 'fog-openstack', '~> 0.3', require: false
-gem 'paperclip', '~> 6.0'
+gem 'kt-paperclip', '~> 7.0'
 gem 'blurhash', '~> 0.1'
 
 gem 'active_model_serializers', '~> 0.10'
diff --git a/Gemfile.lock b/Gemfile.lock
index e4bbe9ce99..42fe0e8f81 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -316,6 +316,12 @@ GEM
       activerecord
       kaminari-core (= 1.2.1)
     kaminari-core (1.2.1)
+    kt-paperclip (7.0.0)
+      activemodel (>= 4.2.0)
+      activesupport (>= 4.2.0)
+      marcel (~> 1.0.1)
+      mime-types
+      terrapin (~> 0.6.0)
     launchy (2.5.0)
       addressable (~> 2.7)
     letter_opener (1.7.0)
@@ -351,9 +357,6 @@ GEM
     mime-types (3.3.1)
       mime-types-data (~> 3.2015)
     mime-types-data (3.2020.0512)
-    mimemagic (0.3.10)
-      nokogiri (~> 1)
-      rake
     mini_mime (1.1.1)
     mini_portile2 (2.6.1)
     minitest (5.14.4)
@@ -391,12 +394,6 @@ GEM
     openssl-signature_algorithm (0.4.0)
     orm_adapter (0.5.0)
     ox (2.14.5)
-    paperclip (6.0.0)
-      activemodel (>= 4.2.0)
-      activesupport (>= 4.2.0)
-      mime-types
-      mimemagic (~> 0.3.0)
-      terrapin (~> 0.6.0)
     parallel (1.21.0)
     parallel_tests (3.7.3)
       parallel
@@ -720,6 +717,7 @@ DEPENDENCIES
   json-ld
   json-ld-preloaded (~> 3.1)
   kaminari (~> 1.2)
+  kt-paperclip (~> 7.0)
   letter_opener (~> 1.7)
   letter_opener_web (~> 1.4)
   link_header (~> 0.0)
@@ -738,7 +736,6 @@ DEPENDENCIES
   omniauth-rails_csrf_protection (~> 0.1)
   omniauth-saml (~> 1.10)
   ox (~> 2.14)
-  paperclip (~> 6.0)
   parallel (~> 1.21)
   parallel_tests (~> 3.7)
   parslet
diff --git a/app/lib/fast_geometry_parser.rb b/app/lib/fast_geometry_parser.rb
index 5209c2bc59..f3395a833d 100644
--- a/app/lib/fast_geometry_parser.rb
+++ b/app/lib/fast_geometry_parser.rb
@@ -2,7 +2,7 @@
 
 class FastGeometryParser
   def self.from_file(file)
-    width, height = FastImage.size(file.path)
+    width, height = FastImage.size(file)
 
     raise Paperclip::Errors::NotIdentifiedByImageMagickError if width.nil?
 
diff --git a/app/models/account.rb b/app/models/account.rb
index 2f2a55b553..291d3e5713 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -62,12 +62,12 @@ class Account < ApplicationRecord
   MENTION_RE    = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[[:word:]]+)?)/i
   URL_PREFIX_RE = /\Ahttp(s?):\/\/[^\/]+/
 
+  include Attachmentable
   include AccountAssociations
   include AccountAvatar
   include AccountFinderConcern
   include AccountHeader
   include AccountInteractions
-  include Attachmentable
   include Paginable
   include AccountCounters
   include DomainNormalizable
diff --git a/app/models/concerns/attachmentable.rb b/app/models/concerns/attachmentable.rb
index c5febb8286..01fae4236f 100644
--- a/app/models/concerns/attachmentable.rb
+++ b/app/models/concerns/attachmentable.rb
@@ -15,50 +15,47 @@ module Attachmentable
   # those files, it is necessary to use the output of the
   # `file` utility instead
   INCORRECT_CONTENT_TYPES = %w(
+    audio/vorbis
     video/ogg
     video/webm
   ).freeze
 
   included do
-    before_post_process :obfuscate_file_name
-    before_post_process :set_file_extensions
-    before_post_process :check_image_dimensions
-    before_post_process :set_file_content_type
+    def self.has_attached_file(name, options = {}) # rubocop:disable Naming/PredicateName
+      options = { validate_media_type: false }.merge(options)
+      super(name, options)
+      send(:"before_#{name}_post_process") do
+        attachment = send(name)
+        check_image_dimension(attachment)
+        set_file_content_type(attachment)
+        obfuscate_file_name(attachment)
+        set_file_extension(attachment)
+        Paperclip::Validators::MediaTypeSpoofDetectionValidator.new(attributes: [name]).validate(self)
+      end
+    end
   end
 
   private
 
-  def set_file_content_type
-    self.class.attachment_definitions.each_key do |attachment_name|
-      attachment = send(attachment_name)
-
-      next if attachment.blank? || attachment.queued_for_write[:original].blank? || !INCORRECT_CONTENT_TYPES.include?(attachment.instance_read(:content_type))
+  def set_file_content_type(attachment) # rubocop:disable Naming/AccessorMethodName
+    return if attachment.blank? || attachment.queued_for_write[:original].blank? || !INCORRECT_CONTENT_TYPES.include?(attachment.instance_read(:content_type))
 
-      attachment.instance_write :content_type, calculated_content_type(attachment)
-    end
+    attachment.instance_write :content_type, calculated_content_type(attachment)
   end
 
-  def set_file_extensions
-    self.class.attachment_definitions.each_key do |attachment_name|
-      attachment = send(attachment_name)
+  def set_file_extension(attachment) # rubocop:disable Naming/AccessorMethodName
+    return if attachment.blank?
 
-      next if attachment.blank?
-
-      attachment.instance_write :file_name, [Paperclip::Interpolations.basename(attachment, :original), appropriate_extension(attachment)].delete_if(&:blank?).join('.')
-    end
+    attachment.instance_write :file_name, [Paperclip::Interpolations.basename(attachment, :original), appropriate_extension(attachment)].delete_if(&:blank?).join('.')
   end
 
-  def check_image_dimensions
-    self.class.attachment_definitions.each_key do |attachment_name|
-      attachment = send(attachment_name)
+  def check_image_dimension(attachment)
+    return if attachment.blank? || !/image.*/.match?(attachment.content_type) || attachment.queued_for_write[:original].blank?
 
-      next if attachment.blank? || !/image.*/.match?(attachment.content_type) || attachment.queued_for_write[:original].blank?
+    width, height = FastImage.size(attachment.queued_for_write[:original].path)
+    matrix_limit  = attachment.content_type == 'image/gif' ? GIF_MATRIX_LIMIT : MAX_MATRIX_LIMIT
 
-      width, height = FastImage.size(attachment.queued_for_write[:original].path)
-      matrix_limit  = attachment.content_type == 'image/gif' ? GIF_MATRIX_LIMIT : MAX_MATRIX_LIMIT
-
-      raise Mastodon::DimensionsValidationError, "#{width}x#{height} images are not supported" if width.present? && height.present? && (width * height > matrix_limit)
-    end
+    raise Mastodon::DimensionsValidationError, "#{width}x#{height} images are not supported" if width.present? && height.present? && (width * height > matrix_limit)
   end
 
   def appropriate_extension(attachment)
@@ -79,13 +76,9 @@ module Attachmentable
     ''
   end
 
-  def obfuscate_file_name
-    self.class.attachment_definitions.each_key do |attachment_name|
-      attachment = send(attachment_name)
+  def obfuscate_file_name(attachment)
+    return if attachment.blank? || attachment.queued_for_write[:original].blank? || attachment.options[:preserve_files]
 
-      next if attachment.blank? || attachment.queued_for_write[:original].blank? || attachment.options[:preserve_files]
-
-      attachment.instance_write :file_name, SecureRandom.hex(8) + File.extname(attachment.instance_read(:file_name))
-    end
+    attachment.instance_write :file_name, SecureRandom.hex(8) + File.extname(attachment.instance_read(:file_name))
   end
 end
diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb
index 7cb03b8199..a85feb73a1 100644
--- a/app/models/custom_emoji.rb
+++ b/app/models/custom_emoji.rb
@@ -21,6 +21,8 @@
 #
 
 class CustomEmoji < ApplicationRecord
+  include Attachmentable
+
   LIMIT = 50.kilobytes
 
   SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}'
@@ -34,7 +36,7 @@ class CustomEmoji < ApplicationRecord
   belongs_to :category, class_name: 'CustomEmojiCategory', optional: true
   has_one :local_counterpart, -> { where(domain: nil) }, class_name: 'CustomEmoji', primary_key: :shortcode, foreign_key: :shortcode
 
-  has_attached_file :image, styles: { static: { format: 'png', convert_options: '-coalesce -strip' } }
+  has_attached_file :image, styles: { static: { format: 'png', convert_options: '-coalesce -strip' } }, validate_media_type: false
 
   before_validation :downcase_domain
 
@@ -49,8 +51,6 @@ class CustomEmoji < ApplicationRecord
 
   remotable_attachment :image, LIMIT
 
-  include Attachmentable
-
   after_commit :remove_entity_cache
 
   def local?
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index 66d800b7b4..cc48f65ed3 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -31,6 +31,8 @@
 class MediaAttachment < ApplicationRecord
   self.inheritance_column = nil
 
+  include Attachmentable
+
   enum type: [:image, :gifv, :video, :unknown, :audio]
   enum processing: [:queued, :in_progress, :complete, :failed], _prefix: true
 
@@ -50,7 +52,7 @@ class MediaAttachment < ApplicationRecord
   IMAGE_MIME_TYPES             = %w(image/jpeg image/png image/gif).freeze
   VIDEO_MIME_TYPES             = %w(video/webm video/mp4 video/quicktime video/ogg).freeze
   VIDEO_CONVERTIBLE_MIME_TYPES = %w(video/webm video/quicktime).freeze
-  AUDIO_MIME_TYPES             = %w(audio/wave audio/wav audio/x-wav audio/x-pn-wave audio/ogg audio/mpeg audio/mp3 audio/webm audio/flac audio/aac audio/m4a audio/x-m4a audio/mp4 audio/3gpp video/x-ms-asf).freeze
+  AUDIO_MIME_TYPES             = %w(audio/wave audio/wav audio/x-wav audio/x-pn-wave audio/ogg audio/vorbis audio/mpeg audio/mp3 audio/webm audio/flac audio/aac audio/m4a audio/x-m4a audio/mp4 audio/3gpp video/x-ms-asf).freeze
 
   BLURHASH_OPTIONS = {
     x_comp: 4,
@@ -182,8 +184,6 @@ class MediaAttachment < ApplicationRecord
   validates_attachment_size :thumbnail, less_than: IMAGE_LIMIT
   remotable_attachment :thumbnail, IMAGE_LIMIT, suppress_errors: true, download_on_assign: false
 
-  include Attachmentable
-
   validates :account, presence: true
   validates :description, length: { maximum: MAX_DESCRIPTION_LENGTH }, if: :local?
   validates :file, presence: true, if: :local?
diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb
index a6ec839f80..bca3a3ce8f 100644
--- a/app/models/preview_card.rb
+++ b/app/models/preview_card.rb
@@ -27,6 +27,8 @@
 #
 
 class PreviewCard < ApplicationRecord
+  include Attachmentable
+
   IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
   LIMIT = 1.megabytes
 
@@ -41,9 +43,7 @@ class PreviewCard < ApplicationRecord
 
   has_and_belongs_to_many :statuses
 
-  has_attached_file :image, processors: [:thumbnail, :blurhash_transcoder], styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 80 -strip' }
-
-  include Attachmentable
+  has_attached_file :image, processors: [:thumbnail, :blurhash_transcoder], styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 80 -strip' }, validate_media_type: false
 
   validates :url, presence: true, uniqueness: true
   validates_attachment_content_type :image, content_type: IMAGE_MIME_TYPES
diff --git a/config/application.rb b/config/application.rb
index 1761bdc2e6..68855a567f 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -25,11 +25,8 @@ require_relative '../lib/exceptions'
 require_relative '../lib/enumerable'
 require_relative '../lib/sanitize_ext/sanitize_config'
 require_relative '../lib/redis/namespace_extensions'
-require_relative '../lib/paperclip/schema_extensions'
-require_relative '../lib/paperclip/validation_extensions'
 require_relative '../lib/paperclip/url_generator_extensions'
 require_relative '../lib/paperclip/attachment_extensions'
-require_relative '../lib/paperclip/media_type_spoof_detector_extensions'
 require_relative '../lib/paperclip/lazy_thumbnail'
 require_relative '../lib/paperclip/gif_transcoder'
 require_relative '../lib/paperclip/transcoder'
diff --git a/lib/paperclip/attachment_extensions.rb b/lib/paperclip/attachment_extensions.rb
index 271f8b6034..786f558e9b 100644
--- a/lib/paperclip/attachment_extensions.rb
+++ b/lib/paperclip/attachment_extensions.rb
@@ -6,6 +6,35 @@ module Paperclip
       instance_read(:meta)
     end
 
+    # monkey-patch to avoid unlinking too avoid unlinking source file too early
+    # see https://github.com/kreeti/kt-paperclip/issues/64
+    def post_process_style(name, style) #:nodoc:
+      raise "Style #{name} has no processors defined." if style.processors.blank?
+
+      intermediate_files = []
+      original = @queued_for_write[:original]
+      # if we're processing the original, close + unlink the source tempfile
+      intermediate_files << original if name == :original
+
+      @queued_for_write[name] = style.processors.
+                                inject(original) do |file, processor|
+        file = Paperclip.processor(processor).make(file, style.processor_options, self)
+        intermediate_files << file unless file == original
+        file
+      end
+
+      unadapted_file = @queued_for_write[name]
+      @queued_for_write[name] = Paperclip.io_adapters.
+                                for(@queued_for_write[name], @options[:adapter_options])
+      unadapted_file.close if unadapted_file.respond_to?(:close)
+      @queued_for_write[name]
+    rescue Paperclip::Errors::NotIdentifiedByImageMagickError => e
+      log("An error was received while processing: #{e.inspect}")
+      (@errors[:processing] ||= []) << e.message if @options[:whiny]
+    ensure
+      unlink_files(intermediate_files)
+    end
+
     # We overwrite this method to support delayed processing in
     # Sidekiq. Since we process the original file to reduce disk
     # usage, and we still want to generate thumbnails straight
diff --git a/lib/paperclip/media_type_spoof_detector_extensions.rb b/lib/paperclip/media_type_spoof_detector_extensions.rb
deleted file mode 100644
index 43337cc688..0000000000
--- a/lib/paperclip/media_type_spoof_detector_extensions.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module Paperclip
-  module MediaTypeSpoofDetectorExtensions
-    def mapping_override_mismatch?
-      !Array(mapped_content_type).include?(calculated_content_type) && !Array(mapped_content_type).include?(type_from_mime_magic)
-    end
-
-    def calculated_media_type_from_mime_magic
-      @calculated_media_type_from_mime_magic ||= type_from_mime_magic.split('/').first
-    end
-
-    def calculated_type_mismatch?
-      !media_types_from_name.include?(calculated_media_type) && !media_types_from_name.include?(calculated_media_type_from_mime_magic)
-    end
-
-    def type_from_mime_magic
-      @type_from_mime_magic ||= begin
-        begin
-          File.open(@file.path) do |file|
-            MimeMagic.by_magic(file)&.type || ''
-          end
-        rescue Errno::ENOENT
-          ''
-        end
-      end
-    end
-
-    def type_from_file_command
-      @type_from_file_command ||= FileCommandContentTypeDetector.new(@file.path).detect
-    end
-  end
-end
-
-Paperclip::MediaTypeSpoofDetector.prepend(Paperclip::MediaTypeSpoofDetectorExtensions)
diff --git a/lib/paperclip/schema_extensions.rb b/lib/paperclip/schema_extensions.rb
deleted file mode 100644
index 8d065676a1..0000000000
--- a/lib/paperclip/schema_extensions.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-# Monkey-patch various Paperclip methods for Ruby 3.0 compatibility
-
-module Paperclip
-  module Schema
-    module StatementsExtensions
-      def add_attachment(table_name, *attachment_names)
-        raise ArgumentError, 'Please specify attachment name in your add_attachment call in your migration.' if attachment_names.empty?
-
-        options = attachment_names.extract_options!
-
-        attachment_names.each do |attachment_name|
-          COLUMNS.each_pair do |column_name, column_type|
-            column_options = options.merge(options[column_name.to_sym] || {})
-            add_column(table_name, "#{attachment_name}_#{column_name}", column_type, **column_options)
-          end
-        end
-      end
-    end
-
-    module TableDefinitionExtensions
-      def attachment(*attachment_names)
-        options = attachment_names.extract_options!
-        attachment_names.each do |attachment_name|
-          COLUMNS.each_pair do |column_name, column_type|
-            column_options = options.merge(options[column_name.to_sym] || {})
-            column("#{attachment_name}_#{column_name}", column_type, **column_options)
-          end
-        end
-      end
-    end
-  end
-end
-
-Paperclip::Schema::Statements.prepend(Paperclip::Schema::StatementsExtensions)
-Paperclip::Schema::TableDefinition.prepend(Paperclip::Schema::TableDefinitionExtensions)
diff --git a/lib/paperclip/url_generator_extensions.rb b/lib/paperclip/url_generator_extensions.rb
index e1d6df2c29..a2cf5929af 100644
--- a/lib/paperclip/url_generator_extensions.rb
+++ b/lib/paperclip/url_generator_extensions.rb
@@ -2,16 +2,6 @@
 
 module Paperclip
   module UrlGeneratorExtensions
-    # Monkey-patch Paperclip to use Addressable::URI's normalization instead
-    # of the long-deprecated URI.esacpe
-    def escape_url(url)
-      if url.respond_to?(:escape)
-        url.escape
-      else
-        Addressable::URI.parse(url).normalize.to_str.gsub(escape_regex) { |m| "%#{m.ord.to_s(16).upcase}" }
-      end
-    end
-
     def for_as_default(style_name)
       attachment_options[:interpolator].interpolate(default_url, @attachment, style_name)
     end
diff --git a/lib/paperclip/validation_extensions.rb b/lib/paperclip/validation_extensions.rb
deleted file mode 100644
index 0df0434f66..0000000000
--- a/lib/paperclip/validation_extensions.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-# frozen_string_literal: true
-
-# Monkey-patch various Paperclip validators for Ruby 3.0 compatibility
-
-module Paperclip
-  module Validators
-    module AttachmentSizeValidatorExtensions
-      def validate_each(record, attr_name, _value)
-        base_attr_name = attr_name
-        attr_name = "#{attr_name}_file_size".to_sym
-        value = record.send(:read_attribute_for_validation, attr_name)
-
-        if value.present?
-          options.slice(*Paperclip::Validators::AttachmentSizeValidator::AVAILABLE_CHECKS).each do |option, option_value|
-            option_value = option_value.call(record) if option_value.is_a?(Proc)
-            option_value = extract_option_value(option, option_value)
-
-            next if value.send(Paperclip::Validators::AttachmentSizeValidator::CHECKS[option], option_value)
-
-            error_message_key = options[:in] ? :in_between : option
-            [attr_name, base_attr_name].each do |error_attr_name|
-              record.errors.add(error_attr_name, error_message_key, **filtered_options(value).merge(
-                min: min_value_in_human_size(record),
-                max: max_value_in_human_size(record),
-                count: human_size(option_value)
-              ))
-            end
-          end
-        end
-      end
-    end
-
-    module AttachmentContentTypeValidatorExtensions
-      def mark_invalid(record, attribute, types)
-        record.errors.add attribute, :invalid, **options.merge({ types: types.join(', ') })
-      end
-    end
-
-    module AttachmentPresenceValidatorExtensions
-      def validate_each(record, attribute, _value)
-        if record.send("#{attribute}_file_name").blank?
-          record.errors.add(attribute, :blank, **options)
-        end
-      end
-    end
-
-    module AttachmentFileNameValidatorExtensions
-      def mark_invalid(record, attribute, patterns)
-        record.errors.add attribute, :invalid, options.merge({ names: patterns.join(', ') })
-      end
-    end
-  end
-end
-
-Paperclip::Validators::AttachmentSizeValidator.prepend(Paperclip::Validators::AttachmentSizeValidatorExtensions)
-Paperclip::Validators::AttachmentContentTypeValidator.prepend(Paperclip::Validators::AttachmentContentTypeValidatorExtensions)
-Paperclip::Validators::AttachmentPresenceValidator.prepend(Paperclip::Validators::AttachmentPresenceValidatorExtensions)
-Paperclip::Validators::AttachmentFileNameValidator.prepend(Paperclip::Validators::AttachmentFileNameValidatorExtensions)
diff --git a/spec/fixtures/files/boop.ogg b/spec/fixtures/files/boop.ogg
new file mode 100644
index 0000000000000000000000000000000000000000..23cbbedb16469381437b6d2ce18cf02b1e2b2be0
GIT binary patch
literal 11379
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
N4Wzk~pzQRM|1ZT8j`{!q

literal 0
HcmV?d00001

diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb
index 456bc42167..edab67d476 100644
--- a/spec/models/media_attachment_spec.rb
+++ b/spec/models/media_attachment_spec.rb
@@ -114,6 +114,30 @@ RSpec.describe MediaAttachment, type: :model do
     end
   end
 
+  describe 'ogg with cover art' do
+    let(:media) { MediaAttachment.create(account: Fabricate(:account), file: attachment_fixture('boop.ogg')) }
+
+    it 'detects it as an audio file' do
+      expect(media.type).to eq 'audio'
+    end
+
+    it 'sets meta for the duration' do
+      expect(media.file.meta['original']['duration']).to be_within(0.05).of(0.235102)
+    end
+
+    it 'extracts thumbnail' do
+      expect(media.thumbnail.present?).to eq true
+    end
+
+    it 'extracts colors from thumbnail' do
+      expect(media.file.meta['colors']['background']).to eq '#3088d4'
+    end
+
+    it 'gives the file a random name' do
+      expect(media.file_file_name).to_not eq 'boop.ogg'
+    end
+  end
+
   describe 'jpeg' do
     let(:media) { MediaAttachment.create(account: Fabricate(:account), file: attachment_fixture('attachment.jpg')) }
 
-- 
GitLab