Repository: igorkasyanchuk/active_storage_validations (Ruby)

Issue:
Validation of SVG content type not working since update active_storage_validations to 1.3.1
Hello everyone,

I installed the latest version (1.3.1) of active_storage_validations  to my Rails 7.0 project today. Since then I get the following errors in the specs:

```
Failure/Error:
     it { is_expected.to validate_content_type_of(:logo).allowing(
       "image/png", "image/gif", "image/jpeg", "image/svg+xml") }
   
     is expected to validate the content types of :logo
       the following content types should be allowed: :image/png, :image/gif, :image/jpeg, :image/svg+xml
       but :image/svg+xml was rejected
```

In my model I configured it like:

```ruby
validates :logo, content_type: ["image/png", "image/jpeg", "image/gif", "image/svg+xml"]
```

Do any of you have a quick guess as to why this might be? I have already tried to change the content type to `:svg`, both in the specs and in the model. But apparently it has a problem with SVGs.


Discussion:
I'm having a similar problem, but with the docx content type. Here is a stand-alone test case that demonstrates the problem:

```ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  gem "rails", "~> 7.0.0"
  gem "sqlite3", "~> 1.4"
  gem "rspec-rails", "~> 7.1"
  gem "marcel", "~> 1.0.4"
  gem "active_storage_validations", "1.3.1"
end

require "active_record/railtie"
require "active_storage/engine"
require "tmpdir"
require "rails/all"
require "rspec/rails"
require "active_storage_validations/matchers"
require "rspec/autorun"

class TestApp < Rails::Application
  config.load_defaults Rails::VERSION::STRING.to_f

  config.root = __dir__
  config.hosts << "example.org"
  config.eager_load = false
  config.session_store :cookie_store, key: "cookie_store_key"
  config.secret_key_base = "secret_key_base"

  config.logger = Logger.new($stdout)
  Rails.logger  = config.logger

  config.active_storage.service = :local
  config.active_storage.service_configurations = {
    local: {
      root: Dir.tmpdir,
      service: "Disk"
    }
  }
end

ENV["DATABASE_URL"] = "sqlite3::memory:"

Rails.application.initialize!

require ActiveStorage::Engine.root.join("db/migrate/20170806125915_create_active_storage_tables.rb").to_s

ActiveRecord::Schema.define do
  CreateActiveStorageTables.new.change

  create_table :documents, force: true
end

class Document < ActiveRecord::Base
  has_one_attached :file
  validates :file, attached: true, content_type: [:docx, :pdf]
end

RSpec.configure do |config|
  config.include ActiveStorageValidations::Matchers, type: :model
end

RSpec.describe Document, type: :model do
  it "validates the correct mime types" do
    document = Document.new
    expect(document).to validate_content_type_of(:file)
      .allowing("application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                "application/pdf")
  end
end
```

Output:

```
Failures:

  1) Document validates the correct mime types
     Failure/Error:
       expect(document).to validate_content_type_of(:file)
         .allowing("application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                   "application/pdf")
     
       is expected to validate the content types of :file
         the following content types should be allowed: :application/vnd.openxmlformats-officedocument.wordprocessingml.document, :application/pdf
         but :application/vnd.openxmlformats-officedocument.wordprocessingml.document was rejected

```
Hi everyone,
It should be released in [1.3.2](https://github.com/igorkasyanchuk/active_storage_validations/releases/tag/1.3.2) in a few hours/days. Check for the release :)

Released with [1.3.2](https://github.com/igorkasyanchuk/active_storage_validations/releases/tag/1.3.2) :)



Patch:
diff --git a/lib/active_storage_validations/matchers/content_type_validator_matcher.rb b/lib/active_storage_validations/matchers/content_type_validator_matcher.rb
index 5dcf43f6..0f177641 100644
--- a/lib/active_storage_validations/matchers/content_type_validator_matcher.rb
+++ b/lib/active_storage_validations/matchers/content_type_validator_matcher.rb
@@ -32,7 +32,7 @@ def initialize(attribute_name)
         initialize_messageable
         initialize_rspecable
         @attribute_name = attribute_name
-        @allowed_types = @rejected_types = []
+        @allowed_content_types = @rejected_content_types = []
       end
 
       def description
@@ -45,13 +45,13 @@ def failure_message
         message.join("\n")
       end
 
-      def allowing(*types)
-        @allowed_types = types.flatten
+      def allowing(*content_types)
+        @allowed_content_types = content_types.flatten
         self
       end
 
-      def rejecting(*types)
-        @rejected_types = types.flatten
+      def rejecting(*content_types)
+        @rejected_content_types = content_types.flatten
         self
       end
 
@@ -62,21 +62,21 @@ def matches?(subject)
           is_context_valid? &&
           is_allowing_blank? &&
           is_custom_message_valid? &&
-          all_allowed_types_allowed? &&
-          all_rejected_types_rejected?
+          all_allowed_content_types_allowed? &&
+          all_rejected_content_types_rejected?
       end
 
       protected
 
       def build_failure_message(message)
-        if @allowed_types_not_allowed.present?
-          message << "  the following content type#{'s' if @allowed_types.count > 1} should be allowed: :#{@allowed_types.join(", :")}"
-          message << "  but #{pluralize(@allowed_types_not_allowed)} rejected"
+        if @allowed_content_types_not_allowed.present?
+          message << "  the following content type#{'s' if @allowed_content_types.count > 1} should be allowed: :#{@allowed_content_types.join(", :")}"
+          message << "  but #{pluralize(@allowed_content_types_not_allowed)} rejected"
         end
 
-        if @rejected_types_not_rejected.present?
-          message << "  the following content type#{'s' if @rejected_types.count > 1} should be rejected: :#{@rejected_types.join(", :")}"
-          message << "  but #{pluralize(@rejected_types_not_rejected)} accepted"
+        if @rejected_content_types_not_rejected.present?
+          message << "  the following content type#{'s' if @rejected_content_types.count > 1} should be rejected: :#{@rejected_content_types.join(", :")}"
+          message << "  but #{pluralize(@rejected_content_types_not_rejected)} accepted"
         end
       end
 
@@ -88,25 +88,25 @@ def pluralize(types)
         end
       end
 
-      def all_allowed_types_allowed?
-        @allowed_types_not_allowed ||= @allowed_types.reject { |type| type_allowed?(type) }
-        @allowed_types_not_allowed.empty?
+      def all_allowed_content_types_allowed?
+        @allowed_content_types_not_allowed ||= @allowed_content_types.reject { |type| type_allowed?(type) }
+        @allowed_content_types_not_allowed.empty?
       end
 
-      def all_rejected_types_rejected?
-        @rejected_types_not_rejected ||= @rejected_types.select { |type| type_allowed?(type) }
-        @rejected_types_not_rejected.empty?
+      def all_rejected_content_types_rejected?
+        @rejected_content_types_not_rejected ||= @rejected_content_types.select { |type| type_allowed?(type) }
+        @rejected_content_types_not_rejected.empty?
       end
 
-      def type_allowed?(type)
-        attach_file_of_type(type)
+      def type_allowed?(content_type)
+        attach_file_with_content_type(content_type)
         validate
         detach_file
         is_valid?
       end
 
-      def attach_file_of_type(type)
-        @subject.public_send(@attribute_name).attach(attachment_for(type))
+      def attach_file_with_content_type(content_type)
+        @subject.public_send(@attribute_name).attach(attachment_for(content_type))
       end
 
       def is_custom_message_valid?
@@ -121,13 +121,13 @@ def attach_invalid_content_type_file
         @subject.public_send(@attribute_name).attach(attachment_for('fake/fake'))
       end
 
-      def attachment_for(type)
-        suffix = type.to_s.split('/').last
+      def attachment_for(content_type)
+        suffix = Marcel::TYPE_EXTS[content_type.to_s]&.first || 'fake'
 
         {
           io: Tempfile.new('.'),
           filename: "test.#{suffix}",
-          content_type: type
+          content_type: content_type
         }
       end
 


Test patch:
diff --git a/test/dummy/app/models/content_type/matcher.rb b/test/dummy/app/models/content_type/matcher.rb
index 18b545b3..9fc75cd0 100644
--- a/test/dummy/app/models/content_type/matcher.rb
+++ b/test/dummy/app/models/content_type/matcher.rb
@@ -23,6 +23,9 @@ class ContentType::Matcher < ApplicationRecord
   has_one_attached :allowing_several_through_regex
   validates :allowing_several_through_regex, content_type: [/\Aimage\/.*\z/]
 
+  has_one_attached :allowing_sneaky_edge_cases
+  validates :allowing_sneaky_edge_cases, content_type: ["image/svg+xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"]
+
   has_one_attached :allow_blank
   validates :allow_blank, content_type: ['image/png'], allow_blank: true
 
diff --git a/test/matchers/content_type_validator_matcher_test.rb b/test/matchers/content_type_validator_matcher_test.rb
index ab9097b3..dee0d222 100644
--- a/test/matchers/content_type_validator_matcher_test.rb
+++ b/test/matchers/content_type_validator_matcher_test.rb
@@ -115,6 +115,19 @@
         it { is_expected_not_to_match_for(klass) }
       end
     end
+
+    describe 'Edge cases' do
+      describe "when the content_type specifier (e.g. 'svg+xml') is not strictly equal to the file extension (e.g. '.svg')" do
+        let(:model_attribute) { :allowing_sneaky_edge_cases }
+        let(:allowed_types) { ["image/svg+xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"] }
+
+        describe 'when provided with the exact allowed types' do
+          subject { matcher.allowing(*allowed_types) }
+
+          it { is_expected_to_match_for(klass) }
+        end
+      end
+    end
   end
 
   describe '#rejecting' do