diff --git a/Gemfile.lock b/Gemfile.lock index df66c8fa..6f39d325 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - active_storage_validations (1.1.0) + active_storage_validations (1.1.1) activejob (>= 5.2.0) activemodel (>= 5.2.0) activestorage (>= 5.2.0) @@ -46,6 +46,7 @@ GEM zeitwerk (~> 2.3) ast (2.4.1) builder (3.2.4) + byebug (11.1.3) coderay (1.1.3) combustion (1.3.1) activesupport (>= 3.0.0) @@ -72,6 +73,8 @@ GEM mini_magick (4.11.0) mini_portile2 (2.5.1) minitest (5.14.3) + minitest-focus (1.4.0) + minitest (>= 4, < 6) nokogiri (1.11.3) mini_portile2 (~> 2.5.0) racc (~> 1.4) @@ -134,10 +137,12 @@ PLATFORMS DEPENDENCIES active_storage_validations! + byebug combustion (~> 1.3) globalid marcel mini_magick (>= 4.9.5) + minitest-focus (~> 1.4) pry rubocop ruby-vips (>= 2.1.0) diff --git a/README.md b/README.md index 97642890..e5e22c29 100755 --- a/README.md +++ b/README.md @@ -368,6 +368,11 @@ BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rake test BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake test ``` +Tips: +- To focus a specific test, use the `focus` class method provided by [minitest-focus](https://github.com/minitest/minitest-focus) +- To focus a specific file, use the TEST option provided by minitest, e.g. to only run size_validator_test.rb file you will execute the following command: `bundle exec rake test TEST=test/validators/size_validator_test.rb`` + + ## Known issues - There is an issue in Rails which it possible to get if you have added a validation and generating for example an image preview of attachments. It can be fixed with this: diff --git a/active_storage_validations.gemspec b/active_storage_validations.gemspec index 5563d205..43073dfd 100644 --- a/active_storage_validations.gemspec +++ b/active_storage_validations.gemspec @@ -19,6 +19,7 @@ Gem::Specification.new do |s| s.files = Dir['{app,config,db,lib}/**/*', 'MIT-LICENSE', 'Rakefile', 'README.md'] %w[activejob activemodel activestorage activesupport].each { |rails_subcomponent| s.add_dependency rails_subcomponent, '>= 5.2.0' } + s.add_development_dependency 'minitest-focus', "~> 1.4" s.add_development_dependency 'combustion', "~> 1.3" s.add_development_dependency 'mini_magick', ">= 4.9.5" s.add_development_dependency 'ruby-vips', ">= 2.1.0" diff --git a/gemfiles/rails_6_0.gemfile.lock b/gemfiles/rails_6_0.gemfile.lock index 0fc90c6d..27146090 100644 --- a/gemfiles/rails_6_0.gemfile.lock +++ b/gemfiles/rails_6_0.gemfile.lock @@ -70,9 +70,15 @@ GEM mini_magick (4.11.0) mini_portile2 (2.8.0) minitest (5.16.3) + minitest-focus (1.4.0) + minitest (>= 4, < 6) nokogiri (1.13.8) mini_portile2 (~> 2.8.0) racc (~> 1.4) + nokogiri (1.13.8-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.13.8-x86_64-linux) + racc (~> 1.4) parallel (1.20.1) parser (3.0.0.0) ast (~> 2.4.1) @@ -141,6 +147,7 @@ DEPENDENCIES globalid marcel mini_magick (>= 4.9.5) + minitest-focus (~> 1.4) pry rubocop ruby-vips (>= 2.1.0) diff --git a/gemfiles/rails_6_1.gemfile.lock b/gemfiles/rails_6_1.gemfile.lock index 648148ea..3b0dd91a 100644 --- a/gemfiles/rails_6_1.gemfile.lock +++ b/gemfiles/rails_6_1.gemfile.lock @@ -72,9 +72,15 @@ GEM mini_magick (4.11.0) mini_portile2 (2.8.0) minitest (5.16.3) + minitest-focus (1.4.0) + minitest (>= 4, < 6) nokogiri (1.13.8) mini_portile2 (~> 2.8.0) racc (~> 1.4) + nokogiri (1.13.8-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.13.8-x86_64-linux) + racc (~> 1.4) parallel (1.20.1) parser (3.0.0.0) ast (~> 2.4.1) @@ -142,6 +148,7 @@ DEPENDENCIES globalid marcel mini_magick (>= 4.9.5) + minitest-focus (~> 1.4) pry rubocop ruby-vips (>= 2.1.0) diff --git a/gemfiles/rails_7_0.gemfile.lock b/gemfiles/rails_7_0.gemfile.lock index 382adb9e..36fd2e48 100644 --- a/gemfiles/rails_7_0.gemfile.lock +++ b/gemfiles/rails_7_0.gemfile.lock @@ -68,6 +68,8 @@ GEM mini_mime (1.1.2) mini_portile2 (2.8.0) minitest (5.16.3) + minitest-focus (1.4.0) + minitest (>= 4, < 6) nokogiri (1.13.8) mini_portile2 (~> 2.8.0) racc (~> 1.4) @@ -137,6 +139,7 @@ DEPENDENCIES globalid marcel mini_magick (>= 4.9.5) + minitest-focus (~> 1.4) pry rubocop ruby-vips (>= 2.1.0) diff --git a/gemfiles/rails_next.gemfile.lock b/gemfiles/rails_next.gemfile.lock index 15aa0f41..3bd981fc 100755 --- a/gemfiles/rails_next.gemfile.lock +++ b/gemfiles/rails_next.gemfile.lock @@ -130,6 +130,8 @@ GEM mini_mime (1.1.2) mini_portile2 (2.8.2) minitest (5.15.0) + minitest-focus (1.4.0) + minitest (>= 4, < 6) net-imap (0.2.3) digest net-protocol @@ -215,6 +217,7 @@ DEPENDENCIES globalid marcel mini_magick (>= 4.9.5) + minitest-focus (~> 1.4) pry rails! rubocop diff --git a/lib/active_storage_validations/aspect_ratio_validator.rb b/lib/active_storage_validations/aspect_ratio_validator.rb index a1959de2..50d29dc6 100644 --- a/lib/active_storage_validations/aspect_ratio_validator.rb +++ b/lib/active_storage_validations/aspect_ratio_validator.rb @@ -1,11 +1,13 @@ # frozen_string_literal: true +require_relative 'concerns/symbolizable.rb' require_relative 'metadata.rb' module ActiveStorageValidations class AspectRatioValidator < ActiveModel::EachValidator # :nodoc include OptionProcUnfolding include ErrorHandler + include Symbolizable AVAILABLE_CHECKS = %i[with].freeze PRECISION = 3 diff --git a/lib/active_storage_validations/attached_validator.rb b/lib/active_storage_validations/attached_validator.rb index 47921400..6cc25bed 100644 --- a/lib/active_storage_validations/attached_validator.rb +++ b/lib/active_storage_validations/attached_validator.rb @@ -1,15 +1,20 @@ # frozen_string_literal: true +require_relative 'concerns/symbolizable.rb' + module ActiveStorageValidations class AttachedValidator < ActiveModel::EachValidator # :nodoc: include ErrorHandler + include Symbolizable + + ERROR_TYPES = %i[blank].freeze def validate_each(record, attribute, _value) return if record.send(attribute).attached? errors_options = initialize_error_options(options) - add_error(record, attribute, :blank, **errors_options) + add_error(record, attribute, ERROR_TYPES.first, **errors_options) end end end diff --git a/lib/active_storage_validations/concerns/symbolizable.rb b/lib/active_storage_validations/concerns/symbolizable.rb new file mode 100644 index 00000000..bd2e7a05 --- /dev/null +++ b/lib/active_storage_validations/concerns/symbolizable.rb @@ -0,0 +1,10 @@ +module Symbolizable + extend ActiveSupport::Concern + + class_methods do + def to_sym + validator_class = self.name.split("::").last + validator_class.sub(/Validator/, '').underscore.to_sym + end + end +end diff --git a/lib/active_storage_validations/content_type_validator.rb b/lib/active_storage_validations/content_type_validator.rb index 6a16d0f7..2975e8e1 100644 --- a/lib/active_storage_validations/content_type_validator.rb +++ b/lib/active_storage_validations/content_type_validator.rb @@ -1,12 +1,16 @@ # frozen_string_literal: true +require_relative 'concerns/symbolizable.rb' + module ActiveStorageValidations class ContentTypeValidator < ActiveModel::EachValidator # :nodoc: include OptionProcUnfolding include ErrorHandler + include Symbolizable AVAILABLE_CHECKS = %i[with in].freeze - + ERROR_TYPES = %i[content_type_invalid].freeze + def validate_each(record, attribute, _value) return true unless record.send(attribute).attached? @@ -22,7 +26,7 @@ def validate_each(record, attribute, _value) next if is_valid?(file, types) errors_options[:content_type] = content_type(file) - add_error(record, attribute, :content_type_invalid, **errors_options) + add_error(record, attribute, ERROR_TYPES.first, **errors_options) break end end diff --git a/lib/active_storage_validations/dimension_validator.rb b/lib/active_storage_validations/dimension_validator.rb index 7c0f6b77..546a6baf 100644 --- a/lib/active_storage_validations/dimension_validator.rb +++ b/lib/active_storage_validations/dimension_validator.rb @@ -1,13 +1,28 @@ # frozen_string_literal: true +require_relative 'concerns/symbolizable.rb' require_relative 'metadata.rb' module ActiveStorageValidations class DimensionValidator < ActiveModel::EachValidator # :nodoc include OptionProcUnfolding include ErrorHandler + include Symbolizable AVAILABLE_CHECKS = %i[width height min max].freeze + ERROR_TYPES = %i[ + image_metadata_missing + dimension_min_inclusion + dimension_max_inclusion + dimension_width_inclusion + dimension_height_inclusion + dimension_width_greater_than_or_equal_to + dimension_height_greater_than_or_equal_to + dimension_width_less_than_or_equal_to + dimension_height_less_than_or_equal_to + dimension_width_equal_to + dimension_height_equal_to + ].freeze def process_options(record) flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS) diff --git a/lib/active_storage_validations/error_handler.rb b/lib/active_storage_validations/error_handler.rb index d16aafc9..496e5981 100644 --- a/lib/active_storage_validations/error_handler.rb +++ b/lib/active_storage_validations/error_handler.rb @@ -3,15 +3,18 @@ module ErrorHandler def initialize_error_options(options) { - message: (options[:message] if options[:message].present?) - } + validator_type: self.class.to_sym, + custom_message: (options[:message] if options[:message].present?) + }.compact end - def add_error(record, attribute, default_message, **errors_options) - message = errors_options[:message].presence || default_message - return if record.errors.added?(attribute, message) + def add_error(record, attribute, error_type, **errors_options) + type = errors_options[:custom_message].presence || error_type + return if record.errors.added?(attribute, type) - record.errors.add(attribute, message, **errors_options) + # You can read https://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-add + # to better understand how Rails model errors work + record.errors.add(attribute, type, **errors_options) end end diff --git a/lib/active_storage_validations/limit_validator.rb b/lib/active_storage_validations/limit_validator.rb index 802ee8bb..cb5fe9e4 100644 --- a/lib/active_storage_validations/limit_validator.rb +++ b/lib/active_storage_validations/limit_validator.rb @@ -1,9 +1,12 @@ # frozen_string_literal: true +require_relative 'concerns/symbolizable.rb' + module ActiveStorageValidations class LimitValidator < ActiveModel::EachValidator # :nodoc: include OptionProcUnfolding include ErrorHandler + include Symbolizable AVAILABLE_CHECKS = %i[max min].freeze diff --git a/lib/active_storage_validations/matchers/attached_validator_matcher.rb b/lib/active_storage_validations/matchers/attached_validator_matcher.rb index 31486ad2..ff5f2665 100644 --- a/lib/active_storage_validations/matchers/attached_validator_matcher.rb +++ b/lib/active_storage_validations/matchers/attached_validator_matcher.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative 'concerns/validatable.rb' + module ActiveStorageValidations module Matchers def validate_attached_of(name) @@ -7,6 +9,8 @@ def validate_attached_of(name) end class AttachedValidatorMatcher + include Validatable + def initialize(attribute_name) @attribute_name = attribute_name @custom_message = nil @@ -23,7 +27,10 @@ def with_message(message) def matches?(subject) @subject = subject.is_a?(Class) ? subject.new : subject - responds_to_methods && valid_when_attached && invalid_when_not_attached + responds_to_methods && + is_valid_when_file_attached? && + is_invalid_when_file_not_attached? && + validate_custom_message? end def failure_message @@ -42,27 +49,44 @@ def responds_to_methods @subject.public_send(@attribute_name).respond_to?(:detach) end - def valid_when_attached - @subject.public_send(@attribute_name).attach(attachable) unless @subject.public_send(@attribute_name).attached? - @subject.validate - @subject.errors.details[@attribute_name].exclude?(error: error_message) + def is_valid_when_file_attached? + attach_dummy_file unless file_attached? + validate + is_valid? end - def invalid_when_not_attached - @subject.public_send(@attribute_name).detach - # Unset the direct relation since `detach` on an unpersisted record does not set `attached?` to false. - @subject.public_send("#{@attribute_name}=", nil) + def is_invalid_when_file_not_attached? + detach_file if file_attached? + validate + !is_valid? + end + + def validate_custom_message? + return true unless @custom_message - @subject.validate - @subject.errors.details[@attribute_name].include?(error: error_message) + detach_file if file_attached? + validate + has_an_error_message_which_is_custom_message? end - def error_message - @custom_message || :blank + def attach_dummy_file + dummy_file = { + io: Tempfile.new('.'), + filename: 'dummy.txt', + content_type: 'text/plain' + } + + @subject.public_send(@attribute_name).attach(dummy_file) + end + + def file_attached? + @subject.public_send(@attribute_name).attached? end - def attachable - { io: Tempfile.new('.'), filename: 'dummy.txt', content_type: 'text/plain' } + def detach_file + @subject.public_send(@attribute_name).detach + # Unset the direct relation since `detach` on an unpersisted record does not set `attached?` to false. + @subject.public_send("#{@attribute_name}=", nil) end end end diff --git a/lib/active_storage_validations/matchers/concerns/validatable.rb b/lib/active_storage_validations/matchers/concerns/validatable.rb new file mode 100644 index 00000000..4a31a9e3 --- /dev/null +++ b/lib/active_storage_validations/matchers/concerns/validatable.rb @@ -0,0 +1,46 @@ +module Validatable + extend ActiveSupport::Concern + + private + + def validate + @subject.validate + end + + def validator_errors_for_attribute + @subject.errors.details[@attribute_name].select do |error| + error[:validator_type] == validator_class.to_sym + end + end + + def is_valid? + validator_errors_for_attribute.none? do |error| + error[:error].in?(available_errors) + end + end + + def available_errors + [ + *validator_class::ERROR_TYPES, + *error_from_custom_message + ].compact + end + + def validator_class + self.class.name.gsub(/::Matchers|Matcher/, '').constantize + end + + def error_from_custom_message + associated_validation = @subject.class.validators_on(@attribute_name).find do |validator| + validator.class == validator_class + end + + associated_validation.options[:message] + end + + def has_an_error_message_which_is_custom_message? + validator_errors_for_attribute.one? do |error| + error[:error] == @custom_message + end + end +end 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 c59075f1..860b4ddb 100644 --- a/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +++ b/lib/active_storage_validations/matchers/content_type_validator_matcher.rb @@ -2,6 +2,9 @@ # Big thank you to the paperclip validation matchers: # https://github.com/thoughtbot/paperclip/blob/v6.1.0/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb + +require_relative 'concerns/validatable.rb' + module ActiveStorageValidations module Matchers def validate_content_type_of(name) @@ -9,8 +12,11 @@ def validate_content_type_of(name) end class ContentTypeValidatorMatcher + include Validatable + def initialize(attribute_name) @attribute_name = attribute_name + @allowed_types = @rejected_types = [] @custom_message = nil end @@ -35,20 +41,24 @@ def with_message(message) def matches?(subject) @subject = subject.is_a?(Class) ? subject.new : subject - responds_to_methods && allowed_types_allowed? && rejected_types_rejected? && validate_custom_message? + + responds_to_methods && + all_allowed_types_allowed? && + all_rejected_types_rejected? && + validate_custom_message? end def failure_message message = ["Expected #{@attribute_name}"] - if @allowed_types - message << "Accept content types: #{allowed_types.join(", ")}" - message << "#{@missing_allowed_types.join(", ")} were rejected" + if @allowed_types_not_allowed.present? + message << "Accept content types: #{@allowed_types.join(", ")}" + message << "#{@allowed_types_not_allowed.join(", ")} were rejected" end - if @rejected_types - message << "Reject content types: #{rejected_types.join(", ")}" - message << "#{@missing_rejected_types.join(", ")} were accepted" + if @rejected_types_not_rejected.present? + message << "Reject content types: #{@rejected_types.join(", ")}" + message << "#{@rejected_types_not_rejected.join(", ")} were accepted" end message.join("\n") @@ -62,49 +72,46 @@ def responds_to_methods @subject.public_send(@attribute_name).respond_to?(:detach) end - def allowed_types - @allowed_types || [] + def all_allowed_types_allowed? + @allowed_types_not_allowed ||= @allowed_types.reject { |type| type_allowed?(type) } + @allowed_types_not_allowed.empty? end - def rejected_types - @rejected_types || [] + def all_rejected_types_rejected? + @rejected_types_not_rejected ||= @rejected_types.select { |type| type_allowed?(type) } + @rejected_types_not_rejected.empty? end - def allowed_types_allowed? - @missing_allowed_types ||= allowed_types.reject { |type| type_allowed?(type) } - @missing_allowed_types.none? - end - - def rejected_types_rejected? - @missing_rejected_types ||= rejected_types.select { |type| type_allowed?(type) } - @missing_rejected_types.none? + def type_allowed?(type) + attach_file_of_type(type) + validate + is_valid? end - def type_allowed?(type) + def attach_file_of_type(type) @subject.public_send(@attribute_name).attach(attachment_for(type)) - @subject.validate - @subject.errors.details[@attribute_name].none? do |error| - error[:error].to_s.include?(error_message) - end end def validate_custom_message? return true unless @custom_message - @subject.public_send(@attribute_name).attach(attachment_for('fake/fake')) - @subject.validate - @subject.errors.details[@attribute_name].select{|error| error[:content_type]}.all? do |error| - error[:error].to_s.include?(error_message) - end + attach_invalid_content_type_file + validate + has_an_error_message_which_is_custom_message? end - def error_message - @custom_message || :content_type_invalid.to_s + 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 - { io: Tempfile.new('.'), filename: "test.#{suffix}", content_type: type } + + { + io: Tempfile.new('.'), + filename: "test.#{suffix}", + content_type: type + } end end end diff --git a/lib/active_storage_validations/matchers/dimension_validator_matcher.rb b/lib/active_storage_validations/matchers/dimension_validator_matcher.rb index 982ca9f7..7eda47fb 100644 --- a/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +++ b/lib/active_storage_validations/matchers/dimension_validator_matcher.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative 'concerns/validatable.rb' + module ActiveStorageValidations module Matchers def validate_dimensions_of(name) @@ -7,6 +9,8 @@ def validate_dimensions_of(name) end class DimensionValidatorMatcher + include Validatable + def initialize(attribute_name) @attribute_name = attribute_name @width_min = @width_max = @height_min = @height_max = nil @@ -64,9 +68,19 @@ def height(height) def matches?(subject) @subject = subject.is_a?(Class) ? subject.new : subject + responds_to_methods && - width_not_smaller_than_min? && width_larger_than_min? && width_smaller_than_max? && width_not_larger_than_max? && width_equals? && - height_not_smaller_than_min? && height_larger_than_min? && height_smaller_than_max? && height_not_larger_than_max? && height_equals? + width_not_smaller_than_min? && + width_larger_than_min? && + width_smaller_than_max? && + width_not_larger_than_max? && + width_equals? && + height_not_smaller_than_min? && + height_larger_than_min? && + height_smaller_than_max? && + height_not_larger_than_max? && + height_equals? && + validate_custom_message? end def failure_message @@ -134,22 +148,38 @@ def height_equals? end def passes_validation_with_dimensions(width, height, check) - @subject.public_send(@attribute_name).attach attachment_for(width, height) + mock_dimensions_for(attach_file, width, height) do + validate + is_valid? + end + end + + def validate_custom_message? + return true unless @custom_message - attachment = @subject.public_send(@attribute_name) + mock_dimensions_for(attach_file, -1, -1) do + validate + has_an_error_message_which_is_custom_message? + end + end + + def mock_dimensions_for(attachment, width, height) Matchers.mock_metadata(attachment, width, height) do - @subject.validate - exclude_error_message = @custom_message || "dimension_#{check}" - @subject.errors.details[@attribute_name].none? do |error| - error[:error].to_s.include?(exclude_error_message) || - error[:error].to_s.include?("dimension_min") || - error[:error].to_s.include?("dimension_max") - end + yield end end - def attachment_for(width, height) - { io: Tempfile.new('Hello world!'), filename: 'test.png', content_type: 'image/png' } + def attach_file + @subject.public_send(@attribute_name).attach(dummy_file) + @subject.public_send(@attribute_name) + end + + def dummy_file + { + io: Tempfile.new('Hello world!'), + filename: 'test.png', + content_type: 'image/png' + } end end end diff --git a/lib/active_storage_validations/matchers/size_validator_matcher.rb b/lib/active_storage_validations/matchers/size_validator_matcher.rb index b234f978..06618417 100644 --- a/lib/active_storage_validations/matchers/size_validator_matcher.rb +++ b/lib/active_storage_validations/matchers/size_validator_matcher.rb @@ -2,6 +2,9 @@ # Big thank you to the paperclip validation matchers: # https://github.com/thoughtbot/paperclip/blob/v6.1.0/lib/paperclip/matchers/validate_attachment_size_matcher.rb + +require_relative 'concerns/validatable.rb' + module ActiveStorageValidations module Matchers def validate_size_of(name) @@ -9,6 +12,8 @@ def validate_size_of(name) end class SizeValidatorMatcher + include Validatable + def initialize(attribute_name) @attribute_name = attribute_name @min = @max = nil @@ -51,7 +56,13 @@ def with_message(message) def matches?(subject) @subject = subject.is_a?(Class) ? subject.new : subject - responds_to_methods && not_lower_than_min? && higher_than_min? && lower_than_max? && not_higher_than_max? + + responds_to_methods && + not_lower_than_min? && + higher_than_min? && + lower_than_max? && + not_higher_than_max? && + validate_custom_message? end def failure_message @@ -86,17 +97,45 @@ def not_higher_than_max? @max.nil? || @max == Float::INFINITY || !passes_validation_with_size(@max + 1) end - def passes_validation_with_size(new_size) - io = Tempfile.new('Hello world!') - Matchers.stub_method(io, :size, new_size) do - @subject.public_send(@attribute_name).attach(io: io, filename: 'test.png', content_type: 'image/pg') - @subject.validate - exclude_error_message = @custom_message || "file_size_not_" - @subject.errors.details[@attribute_name].none? do |error| - error[:error].to_s.include?(exclude_error_message) - end + def passes_validation_with_size(size) + mock_size_for(io, size) do + attach_file + validate + is_valid? end end + + def validate_custom_message? + return true unless @custom_message + + mock_size_for(io, -1.kilobytes) do + attach_file + validate + has_an_error_message_which_is_custom_message? + end + end + + def mock_size_for(io, size) + Matchers.stub_method(io, :size, size) do + yield + end + end + + def attach_file + @subject.public_send(@attribute_name).attach(dummy_file) + end + + def dummy_file + { + io: io, + filename: 'test.png', + content_type: 'image/png' + } + end + + def io + @io ||= Tempfile.new('Hello world!') + end end end end diff --git a/lib/active_storage_validations/processable_image_validator.rb b/lib/active_storage_validations/processable_image_validator.rb index e4c12c7b..ed7390b9 100644 --- a/lib/active_storage_validations/processable_image_validator.rb +++ b/lib/active_storage_validations/processable_image_validator.rb @@ -1,11 +1,13 @@ # frozen_string_literal: true +require_relative 'concerns/symbolizable.rb' require_relative 'metadata.rb' module ActiveStorageValidations class ProcessableImageValidator < ActiveModel::EachValidator # :nodoc include OptionProcUnfolding include ErrorHandler + include Symbolizable if Rails.gem_version >= Gem::Version.new('6.0.0') def validate_each(record, attribute, _value) diff --git a/lib/active_storage_validations/size_validator.rb b/lib/active_storage_validations/size_validator.rb index 67b04350..c90c2207 100644 --- a/lib/active_storage_validations/size_validator.rb +++ b/lib/active_storage_validations/size_validator.rb @@ -1,13 +1,29 @@ # frozen_string_literal: true +require_relative 'concerns/symbolizable.rb' + module ActiveStorageValidations class SizeValidator < ActiveModel::EachValidator # :nodoc: include OptionProcUnfolding include ErrorHandler + include Symbolizable delegate :number_to_human_size, to: ActiveSupport::NumberHelper - AVAILABLE_CHECKS = %i[less_than less_than_or_equal_to greater_than greater_than_or_equal_to between].freeze + AVAILABLE_CHECKS = %i[ + less_than + less_than_or_equal_to + greater_than + greater_than_or_equal_to + between + ].freeze + ERROR_TYPES = %i[ + file_size_not_less_than + file_size_not_less_than_or_equal_to + file_size_not_greater_than + file_size_not_greater_than_or_equal_to + file_size_not_between + ].freeze def check_validity! unless AVAILABLE_CHECKS.one? { |argument| options.key?(argument) } @@ -39,6 +55,8 @@ def validate_each(record, attribute, _value) end def content_size_valid?(file_size, flat_options) + return false if file_size < 0 + if flat_options[:between].present? flat_options[:between].include?(file_size) elsif flat_options[:less_than].present? diff --git a/test/active_storage_validations_test.rb b/test/active_storage_validations_test.rb index 5984f350..53601abb 100644 --- a/test/active_storage_validations_test.rb +++ b/test/active_storage_validations_test.rb @@ -52,12 +52,14 @@ class ActiveStorageValidations::Test < ActiveSupport::TestCase assert_equal u.errors.details, avatar: [ { error: :content_type_invalid, + validator_type: :content_type, authorized_types: 'PNG', content_type: 'text/plain' } ], proc_avatar: [ { error: :content_type_invalid, + validator_type: :content_type, authorized_types: 'PNG', content_type: 'text/plain' } @@ -266,7 +268,7 @@ class ActiveStorageValidations::Test < ActiveSupport::TestCase e.image.attach(html_file) e.proc_image.attach(html_file) assert !e.valid? - assert_equal e.errors.full_messages, ["Image is not a valid image", "Image has an invalid content type", "Proc image is not a valid image", "Proc image has an invalid content type"] + assert_equal e.errors.full_messages, ["Image is not a valid image", "Image is not a valid image", "Image has an invalid content type", "Proc image is not a valid image", "Proc image is not a valid image", "Proc image has an invalid content type"] e = OnlyImage.new e.image.attach(image_1920x1080_file) diff --git a/test/dummy/app/models/attached.rb b/test/dummy/app/models/attached.rb new file mode 100644 index 00000000..1fe5ac84 --- /dev/null +++ b/test/dummy/app/models/attached.rb @@ -0,0 +1,5 @@ +module Attached + def self.table_name_prefix + 'attached_' + end +end diff --git a/test/dummy/app/models/attached/matcher.rb b/test/dummy/app/models/attached/matcher.rb new file mode 100644 index 00000000..cf80c7da --- /dev/null +++ b/test/dummy/app/models/attached/matcher.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: attached_matchers +# +# id :integer not null, primary key +# created_at :datetime not null +# updated_at :datetime not null +# + +class Attached::Matcher < ApplicationRecord + has_one_attached :required + validates :required, attached: true + + has_one_attached :required_with_message + validates :required_with_message, attached: { message: 'Mandatory.' } + + has_one_attached :not_required +end diff --git a/test/dummy/app/models/content_type.rb b/test/dummy/app/models/content_type.rb new file mode 100644 index 00000000..89d14b21 --- /dev/null +++ b/test/dummy/app/models/content_type.rb @@ -0,0 +1,5 @@ +module ContentType + def self.table_name_prefix + 'content_type_' + end +end diff --git a/test/dummy/app/models/content_type/matcher.rb b/test/dummy/app/models/content_type/matcher.rb new file mode 100644 index 00000000..4a1eff4a --- /dev/null +++ b/test/dummy/app/models/content_type/matcher.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: content_type_matchers +# +# id :integer not null, primary key +# created_at :datetime not null +# updated_at :datetime not null +# + +class ContentType::Matcher < ApplicationRecord + has_one_attached :allowing_one + validates :allowing_one, content_type: :png + has_one_attached :allowing_several + validates :allowing_several, content_type: ['image/png', 'image/gif'] + has_one_attached :allowing_several_through_regex + validates :allowing_several_through_regex, content_type: [/\Aimage\/.*\z/] + + has_one_attached :with_message + validates :with_message, content_type: { in: ['image/png'], message: 'Not authorized file type.' } + + # Combinations + has_one_attached :allowing_one_with_message + validates :allowing_one_with_message, content_type: { in: ['file/pdf'], message: 'Not authorized file type.' } +end diff --git a/test/dummy/app/models/dimension.rb b/test/dummy/app/models/dimension.rb new file mode 100644 index 00000000..a1f2a046 --- /dev/null +++ b/test/dummy/app/models/dimension.rb @@ -0,0 +1,5 @@ +module Dimension + def self.table_name_prefix + 'dimension_' + end +end diff --git a/test/dummy/app/models/dimension/matcher.rb b/test/dummy/app/models/dimension/matcher.rb new file mode 100644 index 00000000..0b844ddd --- /dev/null +++ b/test/dummy/app/models/dimension/matcher.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: dimension_matchers +# +# id :integer not null, primary key +# created_at :datetime not null +# updated_at :datetime not null +# + +class Dimension::Matcher < ApplicationRecord + %i(width height).each do |dimension| + has_one_attached :"#{dimension}_exact" + has_one_attached :"#{dimension}_in" + has_one_attached :"#{dimension}_min" + has_one_attached :"#{dimension}_max" + validates :"#{dimension}_exact", dimension: { "#{dimension}": 150 } + validates :"#{dimension}_in", dimension: { "#{dimension}": { in: 800..1200 } } + validates :"#{dimension}_min", dimension: { "#{dimension}": { min: 800 } } + validates :"#{dimension}_max", dimension: { "#{dimension}": { max: 1200 } } + + # Combinations + has_one_attached :"#{dimension}_exact_with_message" + has_one_attached :"#{dimension}_in_with_message" + has_one_attached :"#{dimension}_min_with_message" + has_one_attached :"#{dimension}_max_with_message" + validates :"#{dimension}_exact_with_message", dimension: { "#{dimension}": 150, message: 'Invalid dimensions.' } + validates :"#{dimension}_in_with_message", dimension: { "#{dimension}": { in: 800..1200 }, message: 'Invalid dimensions.' } + validates :"#{dimension}_min_with_message", dimension: { "#{dimension}": { min: 800 }, message: 'Invalid dimensions.' } + validates :"#{dimension}_max_with_message", dimension: { "#{dimension}": { max: 1200 }, message: 'Invalid dimensions.' } + end + + %i(min max).each do |bound| + has_one_attached :"#{bound}" + validates :"#{bound}", dimension: { "#{bound}": 800..600 } + + # Combinations + has_one_attached :"#{bound}_with_message" + validates :"#{bound}_with_message", dimension: { "#{bound}": 800..600, message: 'Invalid dimensions.' } + end + + has_one_attached :with_message + has_one_attached :without_message + validates :with_message, dimension: { width: 150, height: 150, message: 'Invalid dimensions.' } + validates :without_message, dimension: { width: 150, height: 150 } + + # Combinations + has_one_attached :width_and_height_exact + has_one_attached :width_and_height_exact_with_message + validates :width_and_height_exact, dimension: { width: 150, height: 150 } + validates :width_and_height_exact_with_message, dimension: { width: 150, height: 150, message: 'Invalid dimensions.' } + + has_one_attached :width_and_height_in + has_one_attached :width_and_height_in_with_message + validates :width_and_height_in, dimension: { width: { in: 800..1200 }, height: { in: 600..900 } } + validates :width_and_height_in_with_message, dimension: { width: { in: 800..1200 }, height: { in: 600..900 }, message: 'Invalid dimensions.' } + + has_one_attached :width_and_height_min_max + has_one_attached :width_and_height_min_max_with_message + validates :width_and_height_min_max, dimension: { width: { min: 800, max: 1200 }, height: { min: 600, max: 900 } } + validates :width_and_height_min_max_with_message, dimension: { width: { min: 800, max: 1200 }, height: { min: 600, max: 900 }, message: 'Invalid dimensions.' } +end diff --git a/test/dummy/app/models/integration.rb b/test/dummy/app/models/integration.rb new file mode 100644 index 00000000..02c47a73 --- /dev/null +++ b/test/dummy/app/models/integration.rb @@ -0,0 +1,5 @@ +module Integration + def self.table_name_prefix + 'integration_' + end +end diff --git a/test/dummy/app/models/integration/matcher.rb b/test/dummy/app/models/integration/matcher.rb new file mode 100644 index 00000000..df97960d --- /dev/null +++ b/test/dummy/app/models/integration/matcher.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: dimension_matchers +# +# id :integer not null, primary key +# created_at :datetime not null +# updated_at :datetime not null +# + +class Integration::Matcher < ApplicationRecord + has_one_attached :example_1 + validates :example_1, size: { less_than: 10.megabytes, message: 'must be less than 10 MB' }, + content_type: ['image/png', 'image/jpg', 'image/jpeg'] +end diff --git a/test/dummy/app/models/project.rb b/test/dummy/app/models/project.rb index ff8e02f9..4f5d8891 100644 --- a/test/dummy/app/models/project.rb +++ b/test/dummy/app/models/project.rb @@ -33,7 +33,6 @@ class Project < ApplicationRecord validates :documents, limit: { min: 1, max: 3 } validates :dimension_exact, dimension: { width: 150, height: 150 } - validates :dimension_exact_with_message, dimension: { width: 150, height: 150, message: 'Invalid dimensions.' } validates :dimension_range, dimension: { width: { in: 800..1200 }, height: { in: 600..900 } } validates :dimension_min, dimension: { min: 800..600 } validates :dimension_max, dimension: { max: 1200..900 } @@ -42,7 +41,6 @@ class Project < ApplicationRecord validates :proc_documents, limit: { min: -> (record) {1}, max: -> (record) {3} } validates :proc_dimension_exact, dimension: { width: -> (record) {150}, height: -> (record) {150} } - validates :proc_dimension_exact_with_message, dimension: { width: -> (record) {150}, height: -> (record) {150}, message: 'Invalid dimensions.' } validates :proc_dimension_range, dimension: { width: { in: -> (record) {800..1200} }, height: { in: -> (record) {600..900} } } validates :proc_dimension_min, dimension: { min: -> (record) {800..600} } validates :proc_dimension_max, dimension: { max: -> (record) {1200..900} } diff --git a/test/dummy/app/models/size.rb b/test/dummy/app/models/size.rb new file mode 100644 index 00000000..0f23a17d --- /dev/null +++ b/test/dummy/app/models/size.rb @@ -0,0 +1,5 @@ +module Size + def self.table_name_prefix + 'size_' + end +end diff --git a/test/dummy/app/models/size/matcher.rb b/test/dummy/app/models/size/matcher.rb new file mode 100644 index 00000000..8f8e8b0a --- /dev/null +++ b/test/dummy/app/models/size/matcher.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: size_matchers +# +# id :integer not null, primary key +# created_at :datetime not null +# updated_at :datetime not null +# + +class Size::Matcher < ApplicationRecord + has_one_attached :less_than + has_one_attached :less_than_or_equal_to + has_one_attached :greater_than + has_one_attached :greater_than_or_equal_to + has_one_attached :between + validates :less_than, size: { less_than: 2.kilobytes } + validates :less_than_or_equal_to, size: { less_than_or_equal_to: 2.kilobytes } + validates :greater_than, size: { greater_than: 7.kilobytes } + validates :greater_than_or_equal_to, size: { greater_than_or_equal_to: 7.kilobytes } + validates :between, size: { between: 2.kilobytes..7.kilobytes } + + has_one_attached :proc_less_than + has_one_attached :proc_less_than_or_equal_to + has_one_attached :proc_greater_than + has_one_attached :proc_greater_than_or_equal_to + has_one_attached :proc_between + validates :proc_less_than, size: { less_than: -> (record) { 2.kilobytes } } + validates :proc_less_than_or_equal_to, size: { less_than_or_equal_to: -> (record) { 2.kilobytes } } + validates :proc_greater_than, size: { greater_than: -> (record) { 7.kilobytes } } + validates :proc_greater_than_or_equal_to, size: { greater_than_or_equal_to: -> (record) { 7.kilobytes } } + validates :proc_between, size: { between: -> { 2.kilobytes..7.kilobytes } } + + has_one_attached :with_message + validates :with_message, size: { less_than_or_equal_to: 5.megabytes, message: 'File is too big.' } + + # Combinations + has_one_attached :less_than_with_message + has_one_attached :less_than_or_equal_to_with_message + has_one_attached :greater_than_with_message + has_one_attached :greater_than_or_equal_to_with_message + has_one_attached :between_with_message + validates :less_than_with_message, size: { less_than: 2.kilobytes, message: 'File is too big.' } + validates :less_than_or_equal_to_with_message, size: { less_than_or_equal_to: 2.kilobytes, message: 'File is too big.' } + validates :greater_than_with_message, size: { greater_than: 7.kilobytes, message: 'File is too small.' } + validates :greater_than_or_equal_to_with_message, size: { greater_than_or_equal_to: 7.kilobytes, message: 'File is too small.' } + validates :between_with_message, size: { between: 2.kilobytes..7.kilobytes, message: 'File is not in accepted size range.' } +end diff --git a/test/dummy/app/models/size/portfolio.rb b/test/dummy/app/models/size/portfolio.rb index 3f26b8ea..e8bee33b 100644 --- a/test/dummy/app/models/size/portfolio.rb +++ b/test/dummy/app/models/size/portfolio.rb @@ -2,7 +2,7 @@ # == Schema Information # -# Table name: portfolios +# Table name: size_portfolios # # id :integer not null, primary key # title :string diff --git a/test/dummy/app/models/size/several_validator.rb b/test/dummy/app/models/size/several_validator.rb index 9c136ea3..2aa23552 100644 --- a/test/dummy/app/models/size/several_validator.rb +++ b/test/dummy/app/models/size/several_validator.rb @@ -2,7 +2,7 @@ # == Schema Information # -# Table name: several_validators +# Table name: size_several_validators # # id :integer not null, primary key # title :string diff --git a/test/dummy/app/models/size/several_validator_proc.rb b/test/dummy/app/models/size/several_validator_proc.rb index 6f5b270f..81eda4c7 100644 --- a/test/dummy/app/models/size/several_validator_proc.rb +++ b/test/dummy/app/models/size/several_validator_proc.rb @@ -2,7 +2,7 @@ # == Schema Information # -# Table name: several_validators +# Table name: size_several_validator_procs # # id :integer not null, primary key # title :string diff --git a/test/dummy/app/models/size/zero_validator.rb b/test/dummy/app/models/size/zero_validator.rb index 42f23705..3a64c56c 100644 --- a/test/dummy/app/models/size/zero_validator.rb +++ b/test/dummy/app/models/size/zero_validator.rb @@ -2,7 +2,7 @@ # == Schema Information # -# Table name: zero_validators +# Table name: size_zero_validators # # id :integer not null, primary key # title :string diff --git a/test/dummy/app/models/size/zero_validator_proc.rb b/test/dummy/app/models/size/zero_validator_proc.rb index 0f0f399b..29830376 100644 --- a/test/dummy/app/models/size/zero_validator_proc.rb +++ b/test/dummy/app/models/size/zero_validator_proc.rb @@ -2,7 +2,7 @@ # == Schema Information # -# Table name: zero_validator_procs +# Table name: size_zero_validator_procs # # id :integer not null, primary key # title :string diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb index 6b2be953..bf1a8359 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -34,11 +34,31 @@ end end + create_table :attached_matchers, force: :cascade do |t| + t.datetime :created_at, null: false + t.datetime :updated_at, null: false + end + + create_table :content_type_matchers, force: :cascade do |t| + t.datetime :created_at, null: false + t.datetime :updated_at, null: false + end + + create_table :dimension_matchers, force: :cascade do |t| + t.datetime :created_at, null: false + t.datetime :updated_at, null: false + end + create_table :documents, force: :cascade do |t| t.datetime :created_at, precision: 6, null: false t.datetime :updated_at, precision: 6, null: false end + create_table :integration_matchers, force: :cascade do |t| + t.datetime :created_at, null: false + t.datetime :updated_at, null: false + end + create_table :limit_attachments, force: :cascade do |t| t.string :name end @@ -54,40 +74,45 @@ t.datetime :updated_at, null: false end - create_table :portfolios, force: :cascade do |t| - t.string :title + create_table :ratio_models, force: :cascade do |t| + t.string :name + t.datetime :created_at, precision: 6, null: false + t.datetime :updated_at, precision: 6, null: false + end + + create_table :size_matchers, force: :cascade do |t| t.datetime :created_at, null: false t.datetime :updated_at, null: false end - create_table :zero_validators, force: :cascade do |t| + create_table :size_portfolios, force: :cascade do |t| t.string :title t.datetime :created_at, null: false t.datetime :updated_at, null: false end - create_table :zero_validator_procs, force: :cascade do |t| + create_table :size_several_validator_procs, force: :cascade do |t| t.string :title t.datetime :created_at, null: false t.datetime :updated_at, null: false end - create_table :several_validators, force: :cascade do |t| + create_table :size_several_validators, force: :cascade do |t| t.string :title t.datetime :created_at, null: false t.datetime :updated_at, null: false end - create_table :several_validator_procs, force: :cascade do |t| + create_table :size_zero_validator_procs, force: :cascade do |t| t.string :title t.datetime :created_at, null: false t.datetime :updated_at, null: false end - create_table :ratio_models, force: :cascade do |t| - t.string :name - t.datetime :created_at, precision: 6, null: false - t.datetime :updated_at, precision: 6, null: false + create_table :size_zero_validators, force: :cascade do |t| + t.string :title + t.datetime :created_at, null: false + t.datetime :updated_at, null: false end create_table :users, force: :cascade do |t| diff --git a/test/matchers/attached_validator_matcher_test.rb b/test/matchers/attached_validator_matcher_test.rb index 689838cf..d63f8784 100644 --- a/test/matchers/attached_validator_matcher_test.rb +++ b/test/matchers/attached_validator_matcher_test.rb @@ -1,70 +1,71 @@ # frozen_string_literal: true require 'test_helper' +require 'matchers/support/matcher_helpers' require 'active_storage_validations/matchers' -class ActiveStorageValidations::Matchers::AttachedValidatorMatcher::Test < ActiveSupport::TestCase - test 'positive match when providing class' do - matcher = ActiveStorageValidations::Matchers::AttachedValidatorMatcher.new(:avatar) - matcher.with_message("must not be blank") - assert matcher.matches?(User) - end +describe ActiveStorageValidations::Matchers::AttachedValidatorMatcher do + include MatcherHelpers - test 'negative match when providing class' do - matcher = ActiveStorageValidations::Matchers::AttachedValidatorMatcher.new(:image_regex) - refute matcher.matches?(User) - end + let(:matcher) { ActiveStorageValidations::Matchers::AttachedValidatorMatcher.new(model_attribute) } + let(:klass) { Attached::Matcher } - test 'unknown attached when providing class' do - matcher = ActiveStorageValidations::Matchers::AttachedValidatorMatcher.new(:non_existing) - refute matcher.matches?(User) - end + describe '#with_message' do + let(:model_attribute) { :required_with_message } - test 'positive match when providing instance' do - matcher = ActiveStorageValidations::Matchers::AttachedValidatorMatcher.new(:avatar) - matcher.with_message("must not be blank") - assert matcher.matches?(User.new) - end + describe 'when provided with the model validation message' do + subject { matcher.with_message('Mandatory.') } - test 'negative match when providing instance' do - matcher = ActiveStorageValidations::Matchers::AttachedValidatorMatcher.new(:image_regex) - refute matcher.matches?(User.new) - end + it { is_expected_to_match_for(klass) } + end - test 'unknown attached when providing instance' do - matcher = ActiveStorageValidations::Matchers::AttachedValidatorMatcher.new(:non_existing) - refute matcher.matches?(User.new) - end + describe 'when provided with a different message than the model validation message' do + subject { matcher.with_message('') } - test 'positive match with valid conditional validation' do - matcher = ActiveStorageValidations::Matchers::AttachedValidatorMatcher.new(:conditional_image) - assert matcher.matches?(User.new(name: 'Foo')) - end + it { is_expected_not_to_match_for(klass) } + end - test 'negative match with invalid conditional validation' do - matcher = ActiveStorageValidations::Matchers::AttachedValidatorMatcher.new(:conditional_image) - refute matcher.matches?(User.new) + describe 'when not provided with the #with_message matcher method' do + subject { matcher } + + it { is_expected_to_match_for(klass) } + end end - test 'positive match when providing instance with attachment' do - matcher = ActiveStorageValidations::Matchers::AttachedValidatorMatcher.new(:avatar) - matcher.with_message("must not be blank") - user = User.new - user.avatar.attach(io: Tempfile.new('.'), filename: 'image.png', content_type: 'image/png') - user.proc_avatar.attach(io: Tempfile.new('.'), filename: 'image.png', content_type: 'image/png') - assert matcher.matches?(user) + describe 'when the passed model attribute' do + describe 'does not exist' do + subject { matcher } + + let(:model_attribute) { :not_present_in_model } + + it { is_expected_not_to_match_for(klass) } + end + + describe 'does not have an `attached: true` constraint' do + subject { matcher } + + let(:model_attribute) { :not_required } + + it { is_expected_not_to_match_for(klass) } + end + + describe 'has a custom validation error message' do + describe 'but the matcher is not provided with a #with_message' do + subject { matcher } + + let(:model_attribute) { :required_with_message } + + it { is_expected_to_match_for(klass) } + end + end end - test 'positive match when providing persisted instance with attachment' do - matcher = ActiveStorageValidations::Matchers::AttachedValidatorMatcher.new(:avatar) - matcher.with_message("must not be blank") - user = User.create!( - name: 'Pietje', - avatar: { io: Tempfile.new('.'), filename: 'image.png', content_type: 'image/png' }, - photos: [{ io: Tempfile.new('.'), filename: 'image.png', content_type: 'image/png' }], - proc_avatar: { io: Tempfile.new('.'), filename: 'image.png', content_type: 'image/png' }, - proc_photos: [{ io: Tempfile.new('.'), filename: 'image.png', content_type: 'image/png' }] - ) - assert matcher.matches?(user) + describe 'when the matcher is provided with an instance' do + subject { matcher } + + let(:model_attribute) { :required } + let(:instance) { klass.new } + + it { is_expected_to_match_for(instance) } end end diff --git a/test/matchers/content_type_validator_matcher_test.rb b/test/matchers/content_type_validator_matcher_test.rb index ff45431a..89673ba6 100644 --- a/test/matchers/content_type_validator_matcher_test.rb +++ b/test/matchers/content_type_validator_matcher_test.rb @@ -1,98 +1,273 @@ # frozen_string_literal: true require 'test_helper' +require 'matchers/support/matcher_helpers' require 'active_storage_validations/matchers' -class ActiveStorageValidations::Matchers::ContentTypeValidatorMatcher::Test < ActiveSupport::TestCase - test 'positive match on both allowing and rejecting' do - matcher = ActiveStorageValidations::Matchers::ContentTypeValidatorMatcher.new(:avatar) - matcher.allowing('image/png') - matcher.rejecting('image/jpg') +describe ActiveStorageValidations::Matchers::ContentTypeValidatorMatcher do + include MatcherHelpers - assert matcher.matches?(User) - end + let(:matcher) { ActiveStorageValidations::Matchers::ContentTypeValidatorMatcher.new(model_attribute) } + let(:klass) { ContentType::Matcher } - test 'negative match on both allowing and rejecting' do - matcher = ActiveStorageValidations::Matchers::ContentTypeValidatorMatcher.new(:avatar) - matcher.rejecting('image/png') - matcher.allowing('image/jpg') + describe '#allowing' do + describe 'one' do + let(:model_attribute) { :allowing_one } + let(:allowed_type) { 'image/png' } - refute matcher.matches?(User) - end + describe 'when provided with the exact allowed type' do + subject { matcher.allowing(allowed_type) } - test 'positive match when providing class' do - matcher = ActiveStorageValidations::Matchers::ContentTypeValidatorMatcher.new(:avatar) - matcher.allowing('image/png') - assert matcher.matches?(User) - end + it { is_expected_to_match_for(klass) } + end - test 'negative match when providing class' do - matcher = ActiveStorageValidations::Matchers::ContentTypeValidatorMatcher.new(:avatar) - matcher.allowing('image/jpeg') - refute matcher.matches?(User) - end + describe 'when provided with something that is not a valid type' do + subject { matcher.allowing(not_valid_type) } - test 'unknown attached when providing class' do - matcher = ActiveStorageValidations::Matchers::ContentTypeValidatorMatcher.new(:non_existing) - matcher.allowing('image/png') - refute matcher.matches?(User) - end + let(:not_valid_type) { 'not_valid' } - test 'positive match when providing instance' do - matcher = ActiveStorageValidations::Matchers::ContentTypeValidatorMatcher.new(:avatar) - matcher.allowing('image/png') - assert matcher.matches?(User.new) - end + it { is_expected_not_to_match_for(klass) } + end + end - test 'negative match when providing instance' do - matcher = ActiveStorageValidations::Matchers::ContentTypeValidatorMatcher.new(:avatar) - matcher.allowing('image/jpeg') - refute matcher.matches?(User.new) - end + describe 'several' do + let(:model_attribute) { :allowing_several } + let(:allowed_types) { ['image/png', 'image/gif'] } + let(:not_allowed_types) { ['video/mkv', 'file/pdf'] } - test 'unknown attached when providing instance' do - matcher = ActiveStorageValidations::Matchers::ContentTypeValidatorMatcher.new(:non_existing) - matcher.allowing('image/png') - refute matcher.matches?(User.new) - end + describe 'when provided with the exact allowed types' do + subject { matcher.allowing(*allowed_types) } + + it { is_expected_to_match_for(klass) } + end + + describe 'when provided with only allowed types but not all types' do + subject { matcher.allowing(allowed_types.sample) } + + it { is_expected_to_match_for(klass) } + end + + describe 'when provided with allowed and not allowed types' do + subject { matcher.allowing(allowed_types.sample, not_allowed_types.sample) } + + it { is_expected_not_to_match_for(klass) } + end + + describe 'when provided with only not allowed types' do + subject { matcher.allowing(*not_allowed_types) } + + it { is_expected_not_to_match_for(klass) } + end + + describe 'when provided with something that is not a valid type' do + subject { matcher.allowing(not_valid_type) } + + let(:not_valid_type) { 'not_valid' } + + it { is_expected_not_to_match_for(klass) } + end + end - test 'positive match for rejecting' do - matcher = ActiveStorageValidations::Matchers::ContentTypeValidatorMatcher.new(:avatar) - matcher.rejecting('image/jpeg') - assert matcher.matches?(User) + describe 'several through regex' do + let(:model_attribute) { :allowing_several_through_regex } + let(:some_allowed_types) { ['image/png', 'image/gif'] } + let(:not_allowed_types) { ['video/mkv', 'file/pdf'] } + + describe 'when provided with only allowed types but not all types' do + subject { matcher.allowing(*some_allowed_types) } + + it { is_expected_to_match_for(klass) } + end + + describe 'when provided with allowed and not allowed types' do + subject { matcher.allowing(some_allowed_types.sample, not_allowed_types.sample) } + + it { is_expected_not_to_match_for(klass) } + end + + describe 'when provided with only not allowed types' do + subject { matcher.allowing(*not_allowed_types) } + + it { is_expected_not_to_match_for(klass) } + end + + describe 'when provided with something that is not a valid type' do + subject { matcher.allowing(not_valid_type) } + + let(:not_valid_type) { 'not_valid' } + + it { is_expected_not_to_match_for(klass) } + end + end end - test 'negative match for rejecting' do - matcher = ActiveStorageValidations::Matchers::ContentTypeValidatorMatcher.new(:avatar) - matcher.rejecting('image/png') - refute matcher.matches?(User) + describe '#rejecting' do + let(:model_attribute) { :allowing_one } + let(:allowed_type) { 'image/png' } + + describe 'when provided with the exact allowed type' do + subject { matcher.rejecting(allowed_type) } + + it { is_expected_not_to_match_for(klass) } + end + + describe 'when provided with any type but the allowed type' do + subject { matcher.rejecting(any_type) } + + let(:any_type) { 'video/mkv' } + + it { is_expected_to_match_for(klass) } + end + + describe 'when provided with any types but the allowed type' do + subject { matcher.rejecting(*any_types) } + + let(:any_types) { ['video/mkv', 'image/gif'] } + + it { is_expected_to_match_for(klass) } + end + + describe 'when provided with any types and the allowed type' do + subject { matcher.rejecting(*types) } + + let(:any_types) { ['video/mkv', 'image/gif'] } + let(:types) { any_types + [allowed_type] } + + it { is_expected_not_to_match_for(klass) } + end + + describe 'when provided with something that is not a valid type' do + subject { matcher.rejecting(not_valid_type) } + + let(:not_valid_type) { 'not_valid' } + + it { is_expected_to_match_for(klass) } + end end - test 'positive match on subset of accepted content types' do - matcher = ActiveStorageValidations::Matchers::ContentTypeValidatorMatcher.new(:photos) - matcher.allowing('image/png') - assert matcher.matches?(User) + describe '#with_message' do + let(:model_attribute) { :with_message } + + describe 'when provided with the allowed content type' do + before { matcher.allowing('image/png') } + + describe 'and with the message specified in the model validations' do + subject { matcher.with_message('Not authorized file type.') } + + it { is_expected_to_match_for(klass) } + end + + describe 'and with a different message than the one specified in the model validations' do + subject { matcher.with_message('') } + + it { is_expected_not_to_match_for(klass) } + end + + describe 'but without the #with_message matcher method' do + subject { matcher } + + it { is_expected_to_match_for(klass) } + end + end end - test 'matches when combined with a another validator which has errors (file size = 0)' do - matcher = ActiveStorageValidations::Matchers::ContentTypeValidatorMatcher.new(:moon_picture) - matcher.allowing('image/png') - assert matcher.matches?(User) + describe 'Combinations' do + describe '#allowing + #with_message' do + let(:model_attribute) { :allowing_one_with_message } + let(:allowed_type) { 'file/pdf' } + + describe 'when provided with the exact allowed type' do + describe 'and when provided with the message specified in the model validations' do + subject do + matcher.allowing(allowed_type) + matcher.with_message('Not authorized file type.') + end + + it { is_expected_to_match_for(klass) } + end + end + end + + describe '#rejecting + #with_message' do + let(:model_attribute) { :allowing_one_with_message } + let(:not_allowed_type) { 'video/mkv' } + + describe 'when provided with a not allowed type' do + describe 'and when provided with the message specified in the model validations' do + subject do + matcher.rejecting(not_allowed_type) + matcher.with_message('Not authorized file type.') + end + + it { is_expected_to_match_for(klass) } + end + end + end + + describe '#allowing + #rejecting' do + let(:model_attribute) { :allowing_one } + let(:allowed_type) { 'image/png' } + let(:not_allowed_type) { 'video/mkv' } + + describe 'when provided with the exact allowed type' do + describe 'and when provided with a not allowed type specified in the model validations' do + subject do + matcher.allowing(allowed_type) + matcher.rejecting(not_allowed_type) + end + + it { is_expected_to_match_for(klass) } + end + end + end + + describe '#allowing + #rejecting + #with_message' do + let(:model_attribute) { :allowing_one_with_message } + let(:allowed_type) { 'file/pdf' } + let(:not_allowed_type) { 'video/mkv' } + + describe 'when provided with the exact allowed type' do + describe 'and when provided with a not allowed type' do + describe 'and when provided with the message specified in the model validations' do + subject do + matcher.allowing(allowed_type) + matcher.rejecting(not_allowed_type) + matcher.with_message('Not authorized file type.') + end + + it { is_expected_to_match_for(klass) } + end + end + end + end end - class WithMessageMatcher < ActiveStorageValidations::Matchers::ContentTypeValidatorMatcher::Test - test 'matches when provided with the model validation message' do - matcher = ActiveStorageValidations::Matchers::ContentTypeValidatorMatcher.new(:photo_with_messages) - matcher.allowing('image/png') - matcher.with_message('must be an authorized type') - assert matcher.matches?(User) + describe 'when the passed model attribute' do + describe 'does not exist' do + subject { matcher } + + let(:model_attribute) { :not_present_in_model } + + it { is_expected_not_to_match_for(klass) } end - test 'does not match when not provided with the model validation message2' do - matcher = ActiveStorageValidations::Matchers::ContentTypeValidatorMatcher.new(:photo_with_messages) - matcher.allowing('image/png') - matcher.with_message('') - refute matcher.matches?(User) + describe 'has a custom validation error message' do + describe 'but the matcher is not provided with a #with_message' do + subject { matcher } + + let(:model_attribute) { :with_message } + + it { is_expected_to_match_for(klass) } + end end end + + describe 'when the matcher is provided with an instance' do + subject { matcher.with_message('Not authorized file type.') } + + let(:model_attribute) { :with_message } + let(:instance) { klass.new } + + it { is_expected_to_match_for(instance) } + end end diff --git a/test/matchers/dimension_validator_matcher_test.rb b/test/matchers/dimension_validator_matcher_test.rb index f1fb7a7e..87ba7ef2 100644 --- a/test/matchers/dimension_validator_matcher_test.rb +++ b/test/matchers/dimension_validator_matcher_test.rb @@ -1,204 +1,595 @@ # frozen_string_literal: true require 'test_helper' +require 'matchers/support/matcher_helpers' require 'active_storage_validations/matchers' -class ActiveStorageValidations::Matchers::DimensionValidatorMatcher::Test < ActiveSupport::TestCase - test 'width positive match on lower' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_range) - matcher.width_min 800 - assert matcher.matches?(Project) - end +module DimensionValidatorMatcherTest + module DoesNotMatchWithAnyValues + extend ActiveSupport::Concern - test 'width less than lower' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_range) - matcher.width_min 700 - refute matcher.matches?(Project) + included do + include DimensionValidatorMatcherTest::DoesNotMatchWhenLowerValueThanLowerRangeBoundValue + include DimensionValidatorMatcherTest::DoesNotMatchWhenValueEqualToLowerRangeBoundValue + include DimensionValidatorMatcherTest::DoesNotMatchWhenValueEqualToHigherRangeBoundValue + include DimensionValidatorMatcherTest::DoesNotMatchWhenHigherValueThanHigherRangeBoundValue + end end - test 'width higher than lower' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_range) - matcher.width_min 900 - refute matcher.matches?(Project) - end + module DoesNotMatchWhenLowerValueThanLowerRangeBoundValue + extend ActiveSupport::Concern - test 'width positive match on higher' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_range) - matcher.width_max 1200 - assert matcher.matches?(Project) - end + included do + let(:lower_than_lower_range_bound_value) { matcher_method.match?(/_between/) ? 150..200 : 150 } - test 'width less than higher' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_range) - matcher.width_max 1100 - refute matcher.matches?(Project) - end + describe 'when provided with a lower width than the lower range bound width specified in the model validations' do + subject { matcher.public_send(matcher_method, lower_than_lower_range_bound_value) } - test 'width higher than higher' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_range) - matcher.width_max 1300 - refute matcher.matches?(Project) + it { is_expected_not_to_match_for(klass) } + end + end end - test 'width positive exact match' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_exact) - matcher.width 150 - matcher.height 150 # Make sure the validation on height is ok - assert matcher.matches?(Project) - end + module DoesNotMatchWhenValueEqualToLowerRangeBoundValue + extend ActiveSupport::Concern - test 'width positive exact match with custom message' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_exact_with_message) - matcher.width 150 - matcher.height 150 - matcher.with_message('Invalid dimensions.') - assert matcher.matches?(Project) - end + included do + let(:lower_range_bound_value) { matcher_method.match?(/_between/) ? 800..1000 : 800 } - test 'width bigger than exact match' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_exact) - matcher.width 999 - matcher.height 150 # Make sure the validation on height is ok - refute matcher.matches?(Project) - end + describe 'when provided with the exact lower range bound width specified in the model validations' do + subject { matcher.public_send(matcher_method, lower_range_bound_value) } - test 'width smaller than exact match' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_exact) - matcher.width 50 - matcher.height 150 # Make sure the validation on height is ok - refute matcher.matches?(Project) + it { is_expected_not_to_match_for(klass) } + end + end end - test 'height positive match on lower' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_range) - matcher.height_min 600 - assert matcher.matches?(Project) - end + module DoesNotMatchWhenValueEqualToHigherRangeBoundValue + extend ActiveSupport::Concern - test 'height less than lower' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_range) - matcher.height_min 500 - refute matcher.matches?(Project) - end + included do + let(:higher_range_bound_value) { matcher_method.match?(/_between/) ? 1200..1500 : 1200 } - test 'height higher than lower' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_range) - matcher.height_min 700 - refute matcher.matches?(Project) - end + describe 'when provided with the exact higher range bound width specified in the model validations' do + subject { matcher.public_send(matcher_method, higher_range_bound_value) } - test 'height positive match on higher' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_range) - matcher.height_max 900 - assert matcher.matches?(Project) + it { is_expected_not_to_match_for(klass) } + end + end end - test 'height less than higher' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_range) - matcher.height_max 800 - refute matcher.matches?(Project) - end + module DoesNotMatchWhenHigherValueThanHigherRangeBoundValue + extend ActiveSupport::Concern - test 'height higher than higher' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_range) - matcher.height_max 1000 - refute matcher.matches?(Project) - end + included do + let(:higher_than_higher_range_bound_value) { matcher_method.match?(/_between/) ? 9999..10000 : 9999 } - test 'height positive exact match' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_exact) - matcher.width 150 # Make sure the validation on width is ok - matcher.height 150 - assert matcher.matches?(Project) - end + describe 'when provided with a higher width than the higher range bound width specified in the model validations' do + subject { matcher.public_send(matcher_method, higher_than_higher_range_bound_value) } - test 'height bigger than exact match' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_exact) - matcher.width 150 # Make sure the validation on width is ok - matcher.height 999 - refute matcher.matches?(Project) + it { is_expected_not_to_match_for(klass) } + end + end end - test 'smaller smaller than exact match' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_exact) - matcher.width 150 # Make sure the validation on width is ok - matcher.height 50 - refute matcher.matches?(Project) - end + module OnlyMatchWhenExactValue + extend ActiveSupport::Concern - test 'works when providing an instance' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_range) - matcher.width_min 800 - assert matcher.matches?(Project.new) - end + included do + describe 'when provided with a lower width than the width specified in the model validations' do + subject { matcher.public_send(matcher_method, 1) } - test 'unknown attached when providing class' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:non_existing) - matcher.width_min 800 - refute matcher.matches?(Project) - end + it { is_expected_not_to_match_for(klass) } + end - test 'unknown attached when providing instance' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:non_existing) - matcher.width_min 800 - refute matcher.matches?(Project.new) - end + describe 'when provided with the exact width specified in the model validations' do + subject { matcher.public_send(matcher_method, validator_value) } - # width_min and height_min combined - test 'both width_min and height_min on higher combined are a positive match' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_min) - matcher.width_min 800 - matcher.height_min 600 - assert matcher.matches?(Project) - end + it { is_expected_to_match_for(klass) } + end + + describe 'when provided with a higher width than the width specified in the model validations' do + subject { matcher.public_send(matcher_method, 9999) } - test 'width_min less than lower and height_min on higher combined are a negative match' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_min) - matcher.width_min 700 - matcher.height_min 600 - refute matcher.matches?(Project) + it { is_expected_not_to_match_for(klass) } + end + end end - test 'width_min on higher and height_min less than lower combined are a negative match' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_min) - matcher.width_min 800 - matcher.height_min 500 - refute matcher.matches?(Project) + module OnlyMatchWhenExactValues + extend ActiveSupport::Concern + + included do + %i(width height).each do |dimension| + describe "when provided with a lower #{dimension} than the #{dimension} specified in the model validations" do + subject do + matcher.width(dimension == :width ? 1 : 150) + matcher.height(dimension == :height ? 1 : 150) + end + + it { is_expected_not_to_match_for(klass) } + end + end + + describe 'when provided with the exact width and height specified in the model validations' do + subject do + matcher.width(150) + matcher.height(150) + end + + it { is_expected_to_match_for(klass) } + end + + %i(width height).each do |dimension| + describe "when provided with a higher #{dimension} than the #{dimension} specified in the model validations" do + subject do + matcher.width(dimension == :width ? 9999 : 150) + matcher.height(dimension == :height ? 9999 : 150) + end + + it { is_expected_not_to_match_for(klass) } + end + end + end end +end + +describe ActiveStorageValidations::Matchers::DimensionValidatorMatcher do + include MatcherHelpers + + let(:matcher) { ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(model_attribute) } + let(:klass) { Dimension::Matcher } + + %i(width height).each do |dimension| + describe "##{dimension}" do + let(:matcher_method) { dimension } + + describe "when used on a #{dimension} exact validator (e.g. dimension: { #{dimension}: 150 })" do + let(:model_attribute) { :"#{dimension}_exact" } + let(:validator_value) { 150 } + + include DimensionValidatorMatcherTest::OnlyMatchWhenExactValue + end + + describe "when used on a #{dimension} in validator (e.g. dimension: { #{dimension}: { in: 800..1200 } })" do + let(:model_attribute) { :"#{dimension}_in" } + + include DimensionValidatorMatcherTest::DoesNotMatchWithAnyValues + end + + describe "when used on a #{dimension} min validator (e.g. dimension: { #{dimension}: { min: 800 } })" do + let(:model_attribute) { :"#{dimension}_min" } + + include DimensionValidatorMatcherTest::DoesNotMatchWithAnyValues + end + + describe "when used on a #{dimension} max validator (e.g. dimension: { #{dimension}: { max: 1200 } })" do + let(:model_attribute) { :"#{dimension}_max" } + + include DimensionValidatorMatcherTest::DoesNotMatchWithAnyValues + end + end + + describe "##{dimension}_between" do + let(:matcher_method) { :"#{dimension}_between" } + + describe "when used on a #{dimension} exact validator (e.g. dimension: { #{dimension}: 150 })" do + let(:model_attribute) { :width_exact } + let(:validator_value) { 150 } + + include DimensionValidatorMatcherTest::DoesNotMatchWithAnyValues + end + + describe "when used on a #{dimension} in validator (e.g. dimension: { #{dimension}: { in: 800..1200 } })" do + let(:model_attribute) { :"#{dimension}_in" } + + describe "when provided with the exact lower #{dimension} specified in the model validations" do + describe "and the exact higher #{dimension} specified in the model validations" do + subject { matcher.public_send(:"#{dimension}_between", 800..1200) } + + it { is_expected_to_match_for(klass) } + end + + describe "and a lower #{dimension} than the higher #{dimension} specified in the model validations" do + subject { matcher.public_send(:"#{dimension}_between", 800..1000) } + + it { is_expected_not_to_match_for(klass) } + end + + describe "and a higher #{dimension} than the higher #{dimension} specified in the model validations" do + subject { matcher.public_send(:"#{dimension}_between", 800..9999) } + + it { is_expected_not_to_match_for(klass) } + end + end + + describe "when provided with the exact higher #{dimension} specified in the model validations" do + describe "and the exact lowder #{dimension} specified in the model validations" do + subject { matcher.public_send(:"#{dimension}_between", 800..1200) } + + it { is_expected_to_match_for(klass) } + end + + describe "and a lower #{dimension} than the lower #{dimension} specified in the model validations" do + subject { matcher.public_send(:"#{dimension}_between", 1..1200) } + + it { is_expected_not_to_match_for(klass) } + end + + describe "and a higher #{dimension} than the lower #{dimension} specified in the model validations" do + subject { matcher.public_send(:"#{dimension}_between", 1000..1200) } + + it { is_expected_not_to_match_for(klass) } + end + end + end + + describe "when used on a #{dimension} min validator (e.g. dimension: { #{dimension}: { min: 1200 } })" do + let(:model_attribute) { :"#{dimension}_min" } + + include DimensionValidatorMatcherTest::DoesNotMatchWithAnyValues + end + + describe "when used on a #{dimension} max validator (e.g. dimension: { #{dimension}: { max: 1200 } })" do + let(:model_attribute) { :"#{dimension}_max" } + + include DimensionValidatorMatcherTest::DoesNotMatchWithAnyValues + end + end + + describe "##{dimension}_min" do + let(:matcher_method) { :"#{dimension}_min" } + + describe "when used on a #{dimension} exact validator (e.g. dimension: { #{dimension}: 150 })" do + let(:model_attribute) { :"#{dimension}_exact" } + + include DimensionValidatorMatcherTest::DoesNotMatchWithAnyValues + end + + describe "when used on a #{dimension} in validator (e.g. dimension: { #{dimension}: { in: 800..1200 } })" do + let(:model_attribute) { :"#{dimension}_in" } + let(:validator_lower_range_bound_value) { 800 } + + include DimensionValidatorMatcherTest::DoesNotMatchWhenLowerValueThanLowerRangeBoundValue + + describe "when provided with the exact lower range bound #{dimension} specified in the model validations" do + subject { matcher.public_send(matcher_method, validator_lower_range_bound_value) } + + it { is_expected_to_match_for(klass) } + end + + include DimensionValidatorMatcherTest::DoesNotMatchWhenValueEqualToHigherRangeBoundValue + include DimensionValidatorMatcherTest::DoesNotMatchWhenHigherValueThanHigherRangeBoundValue + end + + describe "when used on a #{dimension} min validator (e.g. dimension: { #{dimension}: { min: 800 } })" do + let(:model_attribute) { :"#{dimension}_min" } + let(:validator_value) { 800 } + + include DimensionValidatorMatcherTest::OnlyMatchWhenExactValue + end - test 'both width_min and height_min less than lower combined are a negative match' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_min) - matcher.width_min 700 - matcher.height_min 500 - refute matcher.matches?(Project) + describe "when used on a #{dimension} max validator (e.g. dimension: { #{dimension}: { max: 1200 } })" do + let(:model_attribute) { :"#{dimension}_max" } + + include DimensionValidatorMatcherTest::DoesNotMatchWithAnyValues + end + end + + describe "##{dimension}_max" do + let(:matcher_method) { :"#{dimension}_max" } + + describe "when used on a #{dimension} exact validator (e.g. dimension: { #{dimension}: 150 })" do + let(:model_attribute) { :"#{dimension}_exact" } + + include DimensionValidatorMatcherTest::DoesNotMatchWithAnyValues + end + + describe "when used on a #{dimension} in validator (e.g. dimension: { #{dimension}: { in: 800..1200 } })" do + let(:model_attribute) { :"#{dimension}_in" } + let(:validator_higher_range_bound_value) { 1200 } + + include DimensionValidatorMatcherTest::DoesNotMatchWhenLowerValueThanLowerRangeBoundValue + include DimensionValidatorMatcherTest::DoesNotMatchWhenValueEqualToLowerRangeBoundValue + + describe "when provided with the exact higher range bound #{dimension} specified in the model validations" do + subject { matcher.public_send(matcher_method, validator_higher_range_bound_value) } + + it { is_expected_to_match_for(klass) } + end + + include DimensionValidatorMatcherTest::DoesNotMatchWhenHigherValueThanHigherRangeBoundValue + end + + describe "when used on a #{dimension} min validator (e.g. dimension: { #{dimension}: { min: 800 } })" do + let(:model_attribute) { :"#{dimension}_min" } + + include DimensionValidatorMatcherTest::DoesNotMatchWithAnyValues + end + + describe "when used on a #{dimension} max validator (e.g. dimension: { #{dimension}: { max: 1200 } })" do + let(:model_attribute) { :"#{dimension}_max" } + let(:validator_value) { 1200 } + + include DimensionValidatorMatcherTest::OnlyMatchWhenExactValue + end + end end - # width_max and height_max combined - test 'both width_max and height_max on higher combined are a positive match' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_max) - matcher.width_max 1200 - matcher.height_max 900 - assert matcher.matches?(Project) + describe '#with_message' do + let(:model_attribute) { :with_message } + + describe 'when provided with the exact dimension value(s) specified in the model validations' do + describe 'and with the message specified in the model validations' do + subject do + matcher.width(150) + matcher.height(150) + matcher.with_message('Invalid dimensions.') + end + + it { is_expected_to_match_for(klass) } + end + + describe 'and with a message not matching the message specified in the model validations' do + subject do + matcher.width(150) + matcher.height(150) + matcher.with_message('Invalid message.') + end + + it { is_expected_not_to_match_for(klass) } + end + + describe 'but without the #with_message matcher method' do + subject do + matcher.width(150) + matcher.height(150) + end + + it { is_expected_to_match_for(klass) } + end + end end - test 'width_max higher than higher and height_max on higher combined are a negative match' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_max) - matcher.width_max 1500 - matcher.height_max 900 - refute matcher.matches?(Project) + describe "Combinations" do + %i(width height).each do |dimension| + describe "##{dimension} + #with_message" do + let(:dimension_matcher_method) { dimension } + let(:model_attribute) { :"#{dimension}_exact_with_message" } + + describe "when used on a #{dimension} exact with message validator (e.g. dimension: { #{dimension}: 150, message: 'Invalid dimensions.' })" do + describe "and when provided with the exact #{dimension} and message specified in the model validations" do + subject do + matcher.public_send(dimension_matcher_method, 150) + matcher.with_message('Invalid dimensions.') + end + + it { is_expected_to_match_for(klass) } + end + end + end + + describe "##{dimension}_between + #with_message" do + let(:dimension_matcher_method) { :"#{dimension}_between" } + let(:model_attribute) { :"#{dimension}_in_with_message" } + + describe "when used on a #{dimension} in with message validator (e.g. dimension: { #{dimension}: { in: 800..1200 }, message: 'Invalid dimensions.' })" do + describe "and when provided with the exact #{dimension} range and message specified in the model validations" do + subject do + matcher.public_send(dimension_matcher_method, 800..1200) + matcher.with_message('Invalid dimensions.') + end + + it { is_expected_to_match_for(klass) } + end + end + end + + describe "##{dimension}_min + #with_message" do + let(:dimension_matcher_method) { :"#{dimension}_min" } + let(:model_attribute) { :"#{dimension}_min_with_message" } + + describe "when used on a #{dimension} min with message validator (e.g. dimension: { #{dimension}: { min: 800 }, message: 'Invalid dimensions.' })" do + describe "and when provided with the min #{dimension} and message specified in the model validations" do + subject do + matcher.public_send(dimension_matcher_method, 800) + matcher.with_message('Invalid dimensions.') + end + + it { is_expected_to_match_for(klass) } + end + end + end + + describe "##{dimension}_max + #with_message" do + let(:dimension_matcher_method) { :"#{dimension}_max" } + let(:model_attribute) { :"#{dimension}_max_with_message" } + + describe "when used on a #{dimension} max with message validator (e.g. dimension: { #{dimension}: { max: 1200 }, message: 'Invalid dimensions.' })" do + describe "and when provided with the max #{dimension} and message specified in the model validations" do + subject do + matcher.public_send(dimension_matcher_method, 1200) + matcher.with_message('Invalid dimensions.') + end + + it { is_expected_to_match_for(klass) } + end + end + end + end + + %i(min max).each do |bound| + describe "#width_#{bound} + #height_#{bound}" do + let(:model_attribute) { :"#{bound}" } + + describe "when provided with both lower width and height than the width and height specified in the model validations" do + subject do + matcher.public_send(:"width_#{bound}", 1) + matcher.public_send(:"height_#{bound}", 1) + end + + it { is_expected_not_to_match_for(klass) } + end + + %i(width height).each do |dimension| + describe "when provided with a lower #{dimension} than the #{dimension} specified in the model validations" do + subject do + matcher.public_send(:"width_#{bound}", dimension == :width ? 1 : 800) + matcher.public_send(:"height_#{bound}", dimension == :height ? 1 : 600) + end + + it { is_expected_not_to_match_for(klass) } + end + end + + describe "when provided with the exact width and height specified in the model validations" do + subject do + matcher.public_send(:"width_#{bound}", 800) + matcher.public_send(:"height_#{bound}", 600) + end + + it { is_expected_to_match_for(klass) } + end + + %i(width height).each do |dimension| + describe "when provided with a higher #{dimension} than the #{dimension} specified in the model validations" do + subject do + matcher.public_send(:"width_#{bound}", dimension == :width ? 9999 : 800) + matcher.public_send(:"height_#{bound}", dimension == :height ? 9999 : 600) + end + + it { is_expected_not_to_match_for(klass) } + end + end + + describe "when provided with both higher width and height than the width and height specified in the model validations" do + subject do + matcher.public_send(:"width_#{bound}", 9999) + matcher.public_send(:"height_#{bound}", 9999) + end + + it { is_expected_not_to_match_for(klass) } + end + end + + describe "#width_#{bound} + #height_#{bound} + #with_message" do + let(:model_attribute) { :"#{bound}_with_message" } + + describe "when provided with the exact width and height specified in the model validations" do + describe "and when provided with the message specified in the model validations" do + subject do + matcher.public_send(:"width_#{bound}", 800) + matcher.public_send(:"height_#{bound}", 600) + matcher.with_message('Invalid dimensions.') + end + + it { is_expected_to_match_for(klass) } + end + end + end + end + + describe '#width + #height' do + describe 'when used on a width exact and height exact validator (e.g. dimension: { width: 150, height: 150 })' do + let(:model_attribute) { :width_and_height_exact } + + include DimensionValidatorMatcherTest::OnlyMatchWhenExactValues + end + end + + describe '#width + #height + #with_message' do + let(:model_attribute) { :width_and_height_exact_with_message } + + describe "when used on a width exact and height exact with message validator (e.g. dimension: { width: 150, height: 150, message: 'Invalid dimensions.' })" do + describe 'and when provided with the exact width, height and message specified in the model validations' do + subject do + matcher.width(150) + matcher.height(150) + matcher.with_message('Invalid dimensions.') + end + + it { is_expected_to_match_for(klass) } + end + end + end + + describe "#width_between + #height_between" do + let(:model_attribute) { :width_and_height_in } + + describe "when provided with the width and height ranges specified in the model validations" do + subject do + matcher.width_between(800..1200) + matcher.height_between(600..900) + end + + it { is_expected_to_match_for(klass) } + + describe "when used on a width and height min max validator (e.g. dimension: { width: { min: 800, max: 1200 }, height: { min: 600, max: 900 } })" do + let(:model_attribute) { :width_and_height_min_max } + + it { is_expected_to_match_for(klass) } + end + end + end + + describe "#width_between + #height_between + #with_message" do + let(:model_attribute) { :width_and_height_in_with_message } + + describe "when provided with the exact width and height ranges specified in the model validations" do + describe "and when provided with the message specified in the model validations" do + subject do + matcher.width_between(800..1200) + matcher.height_between(600..900) + matcher.with_message('Invalid dimensions.') + end + + it { is_expected_to_match_for(klass) } + end + end + end + + describe "#width_min + #width_max + #height_min + #height_max" do + let(:model_attribute) { :width_and_height_min_max } + + describe "when provided with the width and height min max specified in the model validations" do + subject do + matcher.width_min(800) + matcher.width_max(1200) + matcher.height_min(600) + matcher.height_max(900) + end + + it { is_expected_to_match_for(klass) } + end + end end - test 'width_max on higher and height_max higher than higher combined are a negative match' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_max) - matcher.width_max 1200 - matcher.height_max 1100 - refute matcher.matches?(Project) + describe 'when the passed model attribute' do + describe 'does not exist' do + subject { matcher.width(150) } + + let(:model_attribute) { :not_present_in_model } + + it { is_expected_not_to_match_for(klass) } + end + + describe 'has a custom validation error message' do + describe 'but the matcher is not provided with a #with_message' do + subject { matcher.width(150) } + + let(:model_attribute) { :width_exact_with_message } + + it { is_expected_to_match_for(klass) } + end + end end - test 'both width_max and height_max higher than higher combined are a negative match' do - matcher = ActiveStorageValidations::Matchers::DimensionValidatorMatcher.new(:dimension_max) - matcher.width_min 1500 - matcher.height_max 1100 - refute matcher.matches?(Project) + describe 'when the matcher is provided with an instance' do + subject { matcher.width(150) } + + let(:model_attribute) { :width_exact } + let(:instance) { klass.new } + + it { is_expected_to_match_for(instance) } end end diff --git a/test/matchers/integration/integration_test.rb b/test/matchers/integration/integration_test.rb new file mode 100644 index 00000000..113b8f69 --- /dev/null +++ b/test/matchers/integration/integration_test.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'test_helper' +require 'matchers/support/matcher_helpers' +require 'active_storage_validations/matchers' + +describe 'Integration tests' do + include MatcherHelpers + + let(:klass) { Integration::Matcher } + let(:matcher_class) { "ActiveStorageValidations::Matchers::#{matcher_type.to_s.camelize}ValidatorMatcher".constantize } + let(:matcher) { matcher_class.new(model_attribute) } + + describe 'example_1' do + # validates :example_1, size: { less_than: 10.megabytes, message: 'must be less than 10 MB' }, + # content_type: ['image/png', 'image/jpg', 'image/jpeg'] + let(:model_attribute) { :example_1 } + + describe 'size matcher' do + let(:matcher_type) { :size } + + describe 'when provided with the size value and size message specified in the model validations' do + subject do + matcher.less_than(10.megabytes) + matcher.with_message('must be less than 10 MB') + end + + it { is_expected_to_match_for(klass) } + end + end + + describe 'content_type matcher' do + let(:matcher_type) { :content_type } + + describe 'when provided with the content_type value specified in the model validations' do + subject do + matcher.allowing('image/png', 'image/jpg', 'image/jpeg') + end + + it { is_expected_to_match_for(klass) } + end + end + end +end diff --git a/test/matchers/size_validator_matcher_test.rb b/test/matchers/size_validator_matcher_test.rb index 1a3b6f80..c80d1317 100644 --- a/test/matchers/size_validator_matcher_test.rb +++ b/test/matchers/size_validator_matcher_test.rb @@ -1,163 +1,268 @@ # frozen_string_literal: true require 'test_helper' +require 'matchers/support/matcher_helpers' require 'active_storage_validations/matchers' -class ActiveStorageValidations::Matchers::SizeValidatorMatcher::Test < ActiveSupport::TestCase +module SizeValidatorMatcherTest + module OnlyMatchWhenExactValue + extend ActiveSupport::Concern - class LessThanMatcher < ActiveStorageValidations::Matchers::SizeValidatorMatcher::Test - test 'matches when provided with the model validation value' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_less_than) - matcher.less_than 2.kilobytes - assert matcher.matches?(Size::Portfolio) - end + included do + describe 'standard validator' do + describe 'when provided with a lower size than the size specified in the model validations' do + subject { matcher.public_send(matcher_method, 0.5.kilobyte) } + + it { is_expected_not_to_match_for(klass) } + end + + describe 'when provided with the exact size specified in the model validations' do + subject { matcher.public_send(matcher_method, validator_value) } + + it { is_expected_to_match_for(klass) } + end + + describe 'when provided with a higher size than the size specified in the model validations' do + subject { matcher.public_send(matcher_method, 99.kilobytes) } + + it { is_expected_not_to_match_for(klass) } + end + end - test 'does not match when provided a higher value than the model validation value' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_less_than) - matcher.less_than 5.kilobytes - refute matcher.matches?(Size::Portfolio) - end + describe 'proc validator' do + let(:matcher) { ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:"proc_#{model_attribute}") } + + describe 'when provided with a lower size than the size specified in the model validations' do + subject { matcher.public_send(matcher_method, 0.5.kilobyte) } + + it { is_expected_not_to_match_for(klass) } + end + + describe 'when provided with the exact size specified in the model validations' do + subject { matcher.public_send(matcher_method, validator_value) } + + it { is_expected_to_match_for(klass) } + end + + describe 'when provided with a higher size than the size specified in the model validations' do + subject { matcher.public_send(matcher_method, 99.kilobytes) } - test 'does not match when provided a lower value than the model validation value' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_less_than) - matcher.less_than 0.5.kilobyte - refute matcher.matches?(Size::Portfolio) + it { is_expected_not_to_match_for(klass) } + end + end end end +end +describe ActiveStorageValidations::Matchers::SizeValidatorMatcher do + include MatcherHelpers - class LessThanOrEqualToMatcher < ActiveStorageValidations::Matchers::SizeValidatorMatcher::Test - test 'matches when provided with the model validation value' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_less_than_or_equal_to) - matcher.less_than_or_equal_to 2.kilobytes - assert matcher.matches?(Size::Portfolio) - end + let(:matcher) { ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(model_attribute) } + let(:klass) { Size::Matcher } - test 'does not match when provided a higher value than the model validation value' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_less_than_or_equal_to) - matcher.less_than_or_equal_to 5.kilobytes - refute matcher.matches?(Size::Portfolio) - end + describe '#less_than' do + let(:matcher_method) { :less_than } + let(:model_attribute) { matcher_method } + let(:validator_value) { 2.kilobytes } - test 'does not match when provided a lower value than the model validation value' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_less_than_or_equal_to) - matcher.less_than_or_equal_to 0.5.kilobyte - refute matcher.matches?(Size::Portfolio) - end + include SizeValidatorMatcherTest::OnlyMatchWhenExactValue end + describe '#less_than_or_equal_to' do + let(:matcher_method) { :less_than_or_equal_to } + let(:model_attribute) { matcher_method } + let(:validator_value) { 2.kilobytes } - class GreaterThanMatcher < ActiveStorageValidations::Matchers::SizeValidatorMatcher::Test - test 'matches when provided with the model validation value' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_greater_than) - matcher.greater_than 7.kilobytes - assert matcher.matches?(Size::Portfolio) - end + include SizeValidatorMatcherTest::OnlyMatchWhenExactValue + end - test 'does not match when provided a higher value than the model validation value' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_greater_than) - matcher.greater_than 10.kilobytes - refute matcher.matches?(Size::Portfolio) - end + describe '#greater_than' do + let(:matcher_method) { :greater_than } + let(:model_attribute) { matcher_method } + let(:validator_value) { 7.kilobytes } - test 'does not match when provided a lower value than the model validation value' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_greater_than) - matcher.greater_than 0.5.kilobyte - refute matcher.matches?(Size::Portfolio) - end + include SizeValidatorMatcherTest::OnlyMatchWhenExactValue + end + + describe '#greater_than_or_equal_to' do + let(:matcher_method) { :greater_than_or_equal_to } + let(:model_attribute) { matcher_method } + let(:validator_value) { 7.kilobytes } + + include SizeValidatorMatcherTest::OnlyMatchWhenExactValue end + describe '#between' do + let(:model_attribute) { :between } + + describe 'when provided with the exact sizes specified in the model validations' do + subject { matcher.between 2.kilobytes..7.kilobytes } - class GreaterThanOrEqualToMatcher < ActiveStorageValidations::Matchers::SizeValidatorMatcher::Test - test 'matches when provided with the model validation value' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_greater_than_or_equal_to) - matcher.greater_than_or_equal_to 7.kilobytes - assert matcher.matches?(Size::Portfolio) + it { is_expected_to_match_for(klass) } end - test 'does not match when provided a higher value than the model validation value' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_greater_than_or_equal_to) - matcher.greater_than_or_equal_to 10.kilobytes - refute matcher.matches?(Size::Portfolio) + describe 'when provided with a higher size than the size specified in the model validations' do + describe 'for the highest possible size' do + subject { matcher.between 2.kilobytes..10.kilobytes } + + it { is_expected_not_to_match_for(klass) } + end + + describe 'for the lowest possible size' do + subject { matcher.between 5.kilobytes..7.kilobytes } + + it { is_expected_not_to_match_for(klass) } + end end - test 'does not match when provided a lower value than the model validation value' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_greater_than_or_equal_to) - matcher.greater_than_or_equal_to 0.5.kilobyte - refute matcher.matches?(Size::Portfolio) + describe 'when provided with a lower size than the size specified in the model validations' do + describe 'for the highest possible size' do + subject { matcher.between 1.kilobytes..7.kilobytes } + + it { is_expected_not_to_match_for(klass) } + end + + describe 'for the lowest possible size' do + subject { matcher.between 1.kilobytes..7.kilobytes } + + it { is_expected_not_to_match_for(klass) } + end + end + + describe 'when provided with both lowest and highest possible sizes different than the model validations' do + subject { matcher.between 4.kilobytes..20.kilobytes } + + it { is_expected_not_to_match_for(klass) } end end + describe '#with_message' do + let(:model_attribute) { :with_message } + + describe 'when provided with the exact size' do + before { matcher.less_than_or_equal_to(5.megabytes) } + + describe 'and with the message specified in the model validations' do + subject { matcher.with_message('File is too big.') } + + it { is_expected_to_match_for(klass) } + end - class BetweenMatcher < ActiveStorageValidations::Matchers::SizeValidatorMatcher::Test - test 'matches when provided with the model validation value' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_between) - matcher.between 2.kilobytes..7.kilobytes - assert matcher.matches?(Size::Portfolio) + describe 'when provided with a different message than the one specified in the model validations' do + subject { matcher.with_message('') } + + it { is_expected_not_to_match_for(klass) } + end + + describe 'but without the #with_message matcher method' do + subject { matcher } + + it { is_expected_to_match_for(klass) } + end end + end + + describe 'Combinations' do + describe '#less_than + #with_message' do + let(:model_attribute) { :less_than_with_message } + + describe 'when provided with the exact size' do + describe 'and when provided with the message specified in the model validations' do + subject do + matcher.less_than 2.kilobytes + matcher.with_message('File is too big.') + end - test 'does not match when provided a higher value than the model validation value for highest possible size' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_between) - matcher.between 2.kilobytes..10.kilobytes - refute matcher.matches?(Size::Portfolio) + it { is_expected_to_match_for(klass) } + end + end end - test 'does not match when provided a lower value than the model validation value for highest possible size' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_between) - matcher.between 1.kilobytes..7.kilobytes - refute matcher.matches?(Size::Portfolio) + describe '#less_than_or_equal_to + #with_message' do + let(:model_attribute) { :less_than_or_equal_to_with_message } + + describe 'when provided with the exact size' do + describe 'and when provided with the message specified in the model validations' do + subject do + matcher.less_than_or_equal_to 2.kilobytes + matcher.with_message('File is too big.') + end + + it { is_expected_to_match_for(klass) } + end + end end - test 'does not match when provided a higher value than the model validation value for lowest possible size' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_between) - matcher.between 5.kilobytes..7.kilobytes - refute matcher.matches?(Size::Portfolio) + describe '#greater_than + #with_message' do + let(:model_attribute) { :greater_than_with_message } + + describe 'when provided with the exact size' do + describe 'and when provided with the message specified in the model validations' do + subject do + matcher.greater_than 7.kilobytes + matcher.with_message('File is too small.') + end + + it { is_expected_to_match_for(klass) } + end + end end - test 'does not match when provided a lower value than the model validation value for lowest possible size' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_between) - matcher.between 1.kilobytes..7.kilobytes - refute matcher.matches?(Size::Portfolio) + describe '#greater_than_or_equal_to + #with_message' do + let(:model_attribute) { :greater_than_or_equal_to_with_message } + + describe 'when provided with the exact size' do + describe 'and when provided with the message specified in the model validations' do + subject do + matcher.greater_than_or_equal_to 7.kilobytes + matcher.with_message('File is too small.') + end + + it { is_expected_to_match_for(klass) } + end + end end - test 'does not match when provided both lowest and highest possible values different than the model validation value' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_between) - matcher.between 4.kilobytes..20.kilobytes - refute matcher.matches?(Size::Portfolio) + describe '#between + #with_message' do + let(:model_attribute) { :between_with_message } + + describe 'when provided with the exact size' do + describe 'and when provided with the message specified in the model validations' do + subject do + matcher.between 2.kilobyte..7.kilobytes + matcher.with_message('File is not in accepted size range.') + end + + it { is_expected_to_match_for(klass) } + end + end end end + describe 'when the passed model attribute has a custom validation error message' do + describe 'but the matcher is not provided with a #with_message' do + subject { matcher.less_than_or_equal_to 5.megabytes } - class WithMessageMatcher < ActiveStorageValidations::Matchers::SizeValidatorMatcher::Test - test 'matches when provided with the model validation message' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_with_message) - matcher.between 2.kilobytes..7.kilobytes - matcher.with_message('is not in required file size range') - assert matcher.matches?(Size::Portfolio) - end + let(:model_attribute) { :with_message } - test 'does not match when not provided with the model validation' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_with_message) - matcher.between 2.kilobytes..7.kilobytes - matcher.with_message('') - refute matcher.matches?(Size::Portfolio) + it { is_expected_to_match_for(klass) } end end + describe 'when the passed model attribute does not exist' do + subject { matcher.less_than 2.kilobytes } - class UnknownAttachedAttribute < ActiveStorageValidations::Matchers::SizeValidatorMatcher::Test - test 'does not match when provided with an unknown attached attribute' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:non_existing) - matcher.greater_than 2.kilobytes - refute matcher.matches?(Size::Portfolio) - end + let(:model_attribute) { :not_present_in_model } + + it { is_expected_not_to_match_for(klass) } end + describe 'when the matcher is provided with an instance' do + subject { matcher.less_than 2.kilobytes } + + let(:model_attribute) { :less_than } + let(:instance) { klass.new } - # Other tests - test 'matches when provided with an instance' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_less_than) - matcher.less_than 2.kilobytes - assert matcher.matches?(Size::Portfolio.new) + it { is_expected_to_match_for(instance) } end end diff --git a/test/matchers/support/matcher_helpers.rb b/test/matchers/support/matcher_helpers.rb new file mode 100644 index 00000000..8fd2addb --- /dev/null +++ b/test/matchers/support/matcher_helpers.rb @@ -0,0 +1,9 @@ +module MatcherHelpers + def is_expected_to_match_for(klass) + subject && assert(subject.matches?(klass)) + end + + def is_expected_not_to_match_for(klass) + subject && refute(subject.matches?(klass)) + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 232e315b..a8b70bcf 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -19,6 +19,8 @@ # Load other test helpers require 'rails/test_help' require 'minitest/mock' +require 'minitest/spec' +require 'minitest/focus' # Filter out Minitest backtrace while allowing backtrace from other libraries # to be shown.