From 5f52e5930d166b09e8d981045765e17c0583e655 Mon Sep 17 00:00:00 2001 From: Mth0158 Date: Thu, 26 Oct 2023 16:10:54 +0200 Subject: [PATCH 01/13] [Test] Add minitest-focus dependency to make testing easier --- Gemfile.lock | 5 ++++- README.md | 2 ++ active_storage_validations.gemspec | 1 + test/test_helper.rb | 1 + 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index df66c8fa..cf3b2598 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) @@ -72,6 +72,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) @@ -138,6 +140,7 @@ DEPENDENCIES 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..557ab085 100755 --- a/README.md +++ b/README.md @@ -368,6 +368,8 @@ BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rake test BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake test ``` +To focus a specific test, use the `focus` class method provided by [minitest-focus](https://github.com/minitest/minitest-focus) + ## 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/test/test_helper.rb b/test/test_helper.rb index 232e315b..d92d99c6 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -19,6 +19,7 @@ # Load other test helpers require 'rails/test_help' require 'minitest/mock' +require 'minitest/focus' # Filter out Minitest backtrace while allowing backtrace from other libraries # to be shown. From a77d64f8316b625a3e7d2014315a20c48a6ac01b Mon Sep 17 00:00:00 2001 From: Mth0158 Date: Fri, 27 Oct 2023 11:18:36 +0200 Subject: [PATCH 02/13] [Test] Refacto tests using minitest/spec for better readability --- test/matchers/size_validator_matcher_test.rb | 225 ++++++++++--------- test/test_helper.rb | 1 + 2 files changed, 124 insertions(+), 102 deletions(-) diff --git a/test/matchers/size_validator_matcher_test.rb b/test/matchers/size_validator_matcher_test.rb index 1a3b6f80..14317a19 100644 --- a/test/matchers/size_validator_matcher_test.rb +++ b/test/matchers/size_validator_matcher_test.rb @@ -3,161 +3,182 @@ require 'test_helper' require 'active_storage_validations/matchers' -class ActiveStorageValidations::Matchers::SizeValidatorMatcher::Test < ActiveSupport::TestCase +describe ActiveStorageValidations::Matchers::SizeValidatorMatcher do + 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 + + let(:matcher) { ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(model_attribute) } + let(:klass) { Size::Portfolio } - 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) + describe '#less_than' do + let(:model_attribute) { :size_less_than } + + describe 'when provided with the exact size specified in the model validations' do + subject { matcher.less_than 2.kilobytes } + + 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_less_than) - matcher.less_than 5.kilobytes - refute matcher.matches?(Size::Portfolio) + describe 'when provided with a higher size than the size specified in the model validations' do + subject { matcher.less_than 5.kilobytes } + + it { is_expected_not_to_match_for(klass) } end - 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) + describe 'when provided with a lower size than the size specified in the model validations' do + subject { matcher.less_than 0.5.kilobyte } + + it { is_expected_not_to_match_for(klass) } end end + describe '#less_than_or_equal_to' do + let(:model_attribute) { :size_less_than_or_equal_to } - 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) + describe 'when provided with the exact size specified in the model validations' do + subject { matcher.less_than_or_equal_to 2.kilobytes } + + 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_less_than_or_equal_to) - matcher.less_than_or_equal_to 5.kilobytes - refute matcher.matches?(Size::Portfolio) + describe 'when provided with a higher size than the size specified in the model validations' do + subject { matcher.less_than_or_equal_to 5.kilobytes } + + it { is_expected_not_to_match_for(klass) } end - 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) + describe 'when provided with a lower size than the size specified in the model validations' do + subject { matcher.less_than_or_equal_to 0.5.kilobyte } + + it { is_expected_not_to_match_for(klass) } end end + describe '#greater_than' do + let(:model_attribute) { :size_greater_than } + + describe 'when provided with the exact size specified in the model validations' do + subject { matcher.greater_than 7.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) + 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) - matcher.greater_than 10.kilobytes - refute matcher.matches?(Size::Portfolio) + describe 'when provided with a higher size than the size specified in the model validations' do + subject { matcher.greater_than 10.kilobytes } + + it { is_expected_not_to_match_for(klass) } end - 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) + describe 'when provided with a lower size than the size specified in the model validations' do + subject { matcher.greater_than 0.5.kilobyte } + + it { is_expected_not_to_match_for(klass) } end end + describe '#greater_than_or_equal_to' do + let(:model_attribute) { :size_greater_than_or_equal_to } - 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) + describe 'when provided with the exact size specified in the model validations' do + subject { matcher.greater_than_or_equal_to 7.kilobytes } + + 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 + subject { matcher.greater_than_or_equal_to 10.kilobytes } + + it { is_expected_not_to_match_for(klass) } 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 + subject { matcher.greater_than_or_equal_to 0.5.kilobyte } + + it { is_expected_not_to_match_for(klass) } end end + describe '#between' do + let(:model_attribute) { :size_between } - 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) - end + describe 'when provided with the exact sizes specified in the model validations' do + subject { matcher.between 2.kilobytes..7.kilobytes } - 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 - 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) - end + 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 - 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 '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 for lowest possible size' do - matcher = ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:size_between) - matcher.between 1.kilobytes..7.kilobytes - 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 - 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 '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 + before { subject.between 2.kilobytes..7.kilobytes } + + let(:model_attribute) { :size_with_message } - 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) + describe 'when provided with the model validation message' do + subject { matcher.with_message('is not in required file size range') } + + it { is_expected_to_match_for(klass) } end - 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) + describe 'when provided with a different message than the model validation message' do + subject { matcher.with_message('') } + + it { is_expected_not_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) { :size_less_than } + let(:instance) { Size::Portfolio.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/test_helper.rb b/test/test_helper.rb index d92d99c6..a8b70bcf 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -19,6 +19,7 @@ # 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 From 414d02bcfdafd76a76cd3fcaf051c572948aa130 Mon Sep 17 00:00:00 2001 From: Mth0158 Date: Fri, 27 Oct 2023 11:44:47 +0200 Subject: [PATCH 03/13] [Gem] Add minitest-focus to gemfiles --- gemfiles/rails_6_0.gemfile.lock | 7 +++++++ gemfiles/rails_6_1.gemfile.lock | 7 +++++++ gemfiles/rails_7_0.gemfile.lock | 3 +++ gemfiles/rails_next.gemfile.lock | 3 +++ 4 files changed, 20 insertions(+) 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 From 3dc3c325327dd9814c5db1009788cdfa5814dedb Mon Sep 17 00:00:00 2001 From: Mth0158 Date: Fri, 27 Oct 2023 13:50:23 +0200 Subject: [PATCH 04/13] [Test] Refacto dimension_validator_matcher --- Gemfile.lock | 2 + test/dummy/app/models/dimension.rb | 5 + test/dummy/app/models/dimension/matcher.rb | 63 ++ test/dummy/app/models/project.rb | 2 - test/dummy/db/schema.rb | 5 + .../dimension_validator_matcher_test.rb | 691 ++++++++++++++---- test/matchers/size_validator_matcher_test.rb | 9 +- test/matchers/support/matcher_helpers.rb | 9 + 8 files changed, 621 insertions(+), 165 deletions(-) create mode 100644 test/dummy/app/models/dimension.rb create mode 100644 test/dummy/app/models/dimension/matcher.rb create mode 100644 test/matchers/support/matcher_helpers.rb diff --git a/Gemfile.lock b/Gemfile.lock index cf3b2598..6f39d325 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) @@ -136,6 +137,7 @@ PLATFORMS DEPENDENCIES active_storage_validations! + byebug combustion (~> 1.3) globalid marcel 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/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/db/schema.rb b/test/dummy/db/schema.rb index 6b2be953..9bd5ef5a 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -34,6 +34,11 @@ end 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 diff --git a/test/matchers/dimension_validator_matcher_test.rb b/test/matchers/dimension_validator_matcher_test.rb index f1fb7a7e..af7af28f 100644 --- a/test/matchers/dimension_validator_matcher_test.rb +++ b/test/matchers/dimension_validator_matcher_test.rb @@ -1,204 +1,583 @@ # 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 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 DoesNotMatchWhenLowerValueThanLowerRangeBoundValue + include DoesNotMatchWhenValueEqualToLowerRangeBoundValue + include DoesNotMatchWhenValueEqualToHigherRangeBoundValue + include 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) + 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) } + + it { is_expected_not_to_match_for(klass) } + end end +end + +module OnlyMatchWhenExactValues + extend ActiveSupport::Concern - 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) + 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 + +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 OnlyMatchWhenExactValue + end + + describe "when used on a #{dimension} in validator (e.g. dimension: { #{dimension}: { in: 800..1200 } })" do + let(:model_attribute) { :"#{dimension}_in" } + + include DoesNotMatchWithAnyValues + end + + describe "when used on a #{dimension} min validator (e.g. dimension: { #{dimension}: { min: 800 } })" do + let(:model_attribute) { :"#{dimension}_min" } + + include DoesNotMatchWithAnyValues + end + + describe "when used on a #{dimension} max validator (e.g. dimension: { #{dimension}: { max: 1200 } })" do + let(:model_attribute) { :"#{dimension}_max" } + + include 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 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 DoesNotMatchWithAnyValues + end + + describe "when used on a #{dimension} max validator (e.g. dimension: { #{dimension}: { max: 1200 } })" do + let(:model_attribute) { :"#{dimension}_max" } + + include 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 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 DoesNotMatchWhenLowerValueThanLowerRangeBoundValue - 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) + 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 DoesNotMatchWhenValueEqualToHigherRangeBoundValue + include 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 OnlyMatchWhenExactValue + end + + describe "when used on a #{dimension} max validator (e.g. dimension: { #{dimension}: { max: 1200 } })" do + let(:model_attribute) { :"#{dimension}_max" } + + include 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 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 DoesNotMatchWhenLowerValueThanLowerRangeBoundValue + include 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 DoesNotMatchWhenHigherValueThanHigherRangeBoundValue + end + + describe "when used on a #{dimension} min validator (e.g. dimension: { #{dimension}: { min: 800 } })" do + let(:model_attribute) { :"#{dimension}_min" } + + include 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 OnlyMatchWhenExactValue + end + end 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) + %i(min max).each do |bound| + describe "##{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 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_not_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 "##{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 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 does not exist' do + subject { matcher.width(150) } + + let(:model_attribute) { :not_present_in_model } + + it { is_expected_not_to_match_for(klass) } 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/size_validator_matcher_test.rb b/test/matchers/size_validator_matcher_test.rb index 14317a19..0f044941 100644 --- a/test/matchers/size_validator_matcher_test.rb +++ b/test/matchers/size_validator_matcher_test.rb @@ -1,16 +1,11 @@ # frozen_string_literal: true require 'test_helper' +require 'matchers/support/matcher_helpers' require 'active_storage_validations/matchers' describe ActiveStorageValidations::Matchers::SizeValidatorMatcher do - 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 + include MatcherHelpers let(:matcher) { ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(model_attribute) } let(:klass) { Size::Portfolio } 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 From 2df99ef9224b2241c56e0d9ddef91888d5845e5f Mon Sep 17 00:00:00 2001 From: Mth0158 Date: Fri, 3 Nov 2023 16:11:52 +0100 Subject: [PATCH 05/13] [Test] Correct syntax error in dimension_validator_matcher_test file --- .../dimension_validator_matcher_test.rb | 110 +++++++++--------- 1 file changed, 54 insertions(+), 56 deletions(-) diff --git a/test/matchers/dimension_validator_matcher_test.rb b/test/matchers/dimension_validator_matcher_test.rb index af7af28f..d556e178 100644 --- a/test/matchers/dimension_validator_matcher_test.rb +++ b/test/matchers/dimension_validator_matcher_test.rb @@ -314,61 +314,6 @@ module OnlyMatchWhenExactValues end end - %i(min max).each do |bound| - describe "##{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 - end - describe '#with_message' do let(:model_attribute) { :with_message } @@ -472,7 +417,60 @@ module OnlyMatchWhenExactValues end %i(min max).each do |bound| - describe "##{bound} + #with_message" do + 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 From 679e5ded3b74da442d1e476854d798e6ee95c884 Mon Sep 17 00:00:00 2001 From: Mth0158 Date: Fri, 3 Nov 2023 17:39:48 +0100 Subject: [PATCH 06/13] [README] Add a testing tip to make contribution easier --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 557ab085..e5e22c29 100755 --- a/README.md +++ b/README.md @@ -368,7 +368,10 @@ BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rake test BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake test ``` -To focus a specific test, use the `focus` class method provided by [minitest-focus](https://github.com/minitest/minitest-focus) +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 From 6841757cc04c8da9bb2b7d8d0ef517849ab4adb4 Mon Sep 17 00:00:00 2001 From: Mth0158 Date: Mon, 6 Nov 2023 15:59:16 +0100 Subject: [PATCH 07/13] [Test] Slight refactoring of size_validator_matcher_test thanks to dimension_validator_matcher_test experience --- .../dimension_validator_matcher_test.rb | 202 +++++++++--------- test/matchers/size_validator_matcher_test.rb | 108 ++++------ 2 files changed, 141 insertions(+), 169 deletions(-) diff --git a/test/matchers/dimension_validator_matcher_test.rb b/test/matchers/dimension_validator_matcher_test.rb index d556e178..96f87454 100644 --- a/test/matchers/dimension_validator_matcher_test.rb +++ b/test/matchers/dimension_validator_matcher_test.rb @@ -4,129 +4,131 @@ require 'matchers/support/matcher_helpers' require 'active_storage_validations/matchers' -module DoesNotMatchWithAnyValues - extend ActiveSupport::Concern - - included do - include DoesNotMatchWhenLowerValueThanLowerRangeBoundValue - include DoesNotMatchWhenValueEqualToLowerRangeBoundValue - include DoesNotMatchWhenValueEqualToHigherRangeBoundValue - include DoesNotMatchWhenHigherValueThanHigherRangeBoundValue +module DimensionValidatorMatcherTest + module DoesNotMatchWithAnyValues + extend ActiveSupport::Concern + + included do + include DimensionValidatorMatcherTest::DoesNotMatchWhenLowerValueThanLowerRangeBoundValue + include DimensionValidatorMatcherTest::DoesNotMatchWhenValueEqualToLowerRangeBoundValue + include DimensionValidatorMatcherTest::DoesNotMatchWhenValueEqualToHigherRangeBoundValue + include DimensionValidatorMatcherTest::DoesNotMatchWhenHigherValueThanHigherRangeBoundValue + end end -end -module DoesNotMatchWhenLowerValueThanLowerRangeBoundValue - extend ActiveSupport::Concern + module DoesNotMatchWhenLowerValueThanLowerRangeBoundValue + extend ActiveSupport::Concern - included do - let(:lower_than_lower_range_bound_value) { matcher_method.match?(/_between/) ? 150..200 : 150 } + included do + let(:lower_than_lower_range_bound_value) { matcher_method.match?(/_between/) ? 150..200 : 150 } - 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) } + 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) } - it { is_expected_not_to_match_for(klass) } + it { is_expected_not_to_match_for(klass) } + end end end -end -module DoesNotMatchWhenValueEqualToLowerRangeBoundValue - extend ActiveSupport::Concern + module DoesNotMatchWhenValueEqualToLowerRangeBoundValue + extend ActiveSupport::Concern - included do - let(:lower_range_bound_value) { matcher_method.match?(/_between/) ? 800..1000 : 800 } + included do + let(:lower_range_bound_value) { matcher_method.match?(/_between/) ? 800..1000 : 800 } - 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) } + 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) } - it { is_expected_not_to_match_for(klass) } + it { is_expected_not_to_match_for(klass) } + end end end -end -module DoesNotMatchWhenValueEqualToHigherRangeBoundValue - extend ActiveSupport::Concern + module DoesNotMatchWhenValueEqualToHigherRangeBoundValue + extend ActiveSupport::Concern - included do - let(:higher_range_bound_value) { matcher_method.match?(/_between/) ? 1200..1500 : 1200 } + included do + let(:higher_range_bound_value) { matcher_method.match?(/_between/) ? 1200..1500 : 1200 } - 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) } + 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) } - it { is_expected_not_to_match_for(klass) } + it { is_expected_not_to_match_for(klass) } + end end end -end -module DoesNotMatchWhenHigherValueThanHigherRangeBoundValue - extend ActiveSupport::Concern + module DoesNotMatchWhenHigherValueThanHigherRangeBoundValue + extend ActiveSupport::Concern - included do - let(:higher_than_higher_range_bound_value) { matcher_method.match?(/_between/) ? 9999..10000 : 9999 } + included do + let(:higher_than_higher_range_bound_value) { matcher_method.match?(/_between/) ? 9999..10000 : 9999 } - 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) } + 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) } - it { is_expected_not_to_match_for(klass) } + it { is_expected_not_to_match_for(klass) } + end end end -end -module OnlyMatchWhenExactValue - extend ActiveSupport::Concern + module OnlyMatchWhenExactValue + extend ActiveSupport::Concern - 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) } + 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) } - it { is_expected_not_to_match_for(klass) } - end + it { is_expected_not_to_match_for(klass) } + end - describe 'when provided with the exact width specified in the model validations' do - subject { matcher.public_send(matcher_method, validator_value) } + describe 'when provided with the exact width specified in the model validations' do + subject { matcher.public_send(matcher_method, validator_value) } - it { is_expected_to_match_for(klass) } - 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) } + describe 'when provided with a higher width than the width specified in the model validations' do + subject { matcher.public_send(matcher_method, 9999) } - it { is_expected_not_to_match_for(klass) } + it { is_expected_not_to_match_for(klass) } + end end end -end -module OnlyMatchWhenExactValues - extend ActiveSupport::Concern + 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 + 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) } + it { is_expected_not_to_match_for(klass) } + end 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) + 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 - 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 - %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) + it { is_expected_not_to_match_for(klass) } end - - it { is_expected_not_to_match_for(klass) } end end end @@ -146,25 +148,25 @@ module OnlyMatchWhenExactValues let(:model_attribute) { :"#{dimension}_exact" } let(:validator_value) { 150 } - include OnlyMatchWhenExactValue + 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 DoesNotMatchWithAnyValues + 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 DoesNotMatchWithAnyValues + 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 DoesNotMatchWithAnyValues + include DimensionValidatorMatcherTest::DoesNotMatchWithAnyValues end end @@ -175,7 +177,7 @@ module OnlyMatchWhenExactValues let(:model_attribute) { :width_exact } let(:validator_value) { 150 } - include DoesNotMatchWithAnyValues + include DimensionValidatorMatcherTest::DoesNotMatchWithAnyValues end describe "when used on a #{dimension} in validator (e.g. dimension: { #{dimension}: { in: 800..1200 } })" do @@ -225,13 +227,13 @@ module OnlyMatchWhenExactValues describe "when used on a #{dimension} min validator (e.g. dimension: { #{dimension}: { min: 1200 } })" do let(:model_attribute) { :"#{dimension}_min" } - include DoesNotMatchWithAnyValues + 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 DoesNotMatchWithAnyValues + include DimensionValidatorMatcherTest::DoesNotMatchWithAnyValues end end @@ -241,14 +243,14 @@ module OnlyMatchWhenExactValues describe "when used on a #{dimension} exact validator (e.g. dimension: { #{dimension}: 150 })" do let(:model_attribute) { :"#{dimension}_exact" } - include DoesNotMatchWithAnyValues + 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 DoesNotMatchWhenLowerValueThanLowerRangeBoundValue + 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) } @@ -256,21 +258,21 @@ module OnlyMatchWhenExactValues it { is_expected_to_match_for(klass) } end - include DoesNotMatchWhenValueEqualToHigherRangeBoundValue - include DoesNotMatchWhenHigherValueThanHigherRangeBoundValue + 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 OnlyMatchWhenExactValue + include DimensionValidatorMatcherTest::OnlyMatchWhenExactValue end describe "when used on a #{dimension} max validator (e.g. dimension: { #{dimension}: { max: 1200 } })" do let(:model_attribute) { :"#{dimension}_max" } - include DoesNotMatchWithAnyValues + include DimensionValidatorMatcherTest::DoesNotMatchWithAnyValues end end @@ -280,15 +282,15 @@ module OnlyMatchWhenExactValues describe "when used on a #{dimension} exact validator (e.g. dimension: { #{dimension}: 150 })" do let(:model_attribute) { :"#{dimension}_exact" } - include DoesNotMatchWithAnyValues + 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 DoesNotMatchWhenLowerValueThanLowerRangeBoundValue - include DoesNotMatchWhenValueEqualToLowerRangeBoundValue + 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) } @@ -296,20 +298,20 @@ module OnlyMatchWhenExactValues it { is_expected_to_match_for(klass) } end - include DoesNotMatchWhenHigherValueThanHigherRangeBoundValue + 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 DoesNotMatchWithAnyValues + 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 OnlyMatchWhenExactValue + include DimensionValidatorMatcherTest::OnlyMatchWhenExactValue end end end @@ -491,7 +493,7 @@ module OnlyMatchWhenExactValues 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 OnlyMatchWhenExactValues + include DimensionValidatorMatcherTest::OnlyMatchWhenExactValues end end diff --git a/test/matchers/size_validator_matcher_test.rb b/test/matchers/size_validator_matcher_test.rb index 0f044941..eadba2b7 100644 --- a/test/matchers/size_validator_matcher_test.rb +++ b/test/matchers/size_validator_matcher_test.rb @@ -4,98 +4,68 @@ require 'matchers/support/matcher_helpers' require 'active_storage_validations/matchers' -describe ActiveStorageValidations::Matchers::SizeValidatorMatcher do - include MatcherHelpers +module SizeValidatorMatcherTest + module OnlyMatchWhenExactValue + extend ActiveSupport::Concern - let(:matcher) { ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(model_attribute) } - let(:klass) { Size::Portfolio } + included 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) } - describe '#less_than' do - let(:model_attribute) { :size_less_than } - - describe 'when provided with the exact size specified in the model validations' do - subject { matcher.less_than 2.kilobytes } - - it { is_expected_to_match_for(klass) } - end + it { is_expected_not_to_match_for(klass) } + end - describe 'when provided with a higher size than the size specified in the model validations' do - subject { matcher.less_than 5.kilobytes } + describe 'when provided with the exact size specified in the model validations' do + subject { matcher.public_send(matcher_method, validator_value) } - it { is_expected_not_to_match_for(klass) } - end + it { is_expected_to_match_for(klass) } + end - describe 'when provided with a lower size than the size specified in the model validations' do - subject { matcher.less_than 0.5.kilobyte } + 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) } + it { is_expected_not_to_match_for(klass) } + end end end +end - describe '#less_than_or_equal_to' do - let(:model_attribute) { :size_less_than_or_equal_to } - - describe 'when provided with the exact size specified in the model validations' do - subject { matcher.less_than_or_equal_to 2.kilobytes } +describe ActiveStorageValidations::Matchers::SizeValidatorMatcher do + include MatcherHelpers - it { is_expected_to_match_for(klass) } - end + let(:matcher) { ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(model_attribute) } + let(:klass) { Size::Portfolio } - describe 'when provided with a higher size than the size specified in the model validations' do - subject { matcher.less_than_or_equal_to 5.kilobytes } + describe '#less_than' do + let(:model_attribute) { :size_less_than } + let(:matcher_method) { :less_than } + let(:validator_value) { 2.kilobytes } - it { is_expected_not_to_match_for(klass) } - end + include SizeValidatorMatcherTest::OnlyMatchWhenExactValue + end - describe 'when provided with a lower size than the size specified in the model validations' do - subject { matcher.less_than_or_equal_to 0.5.kilobyte } + describe '#less_than_or_equal_to' do + let(:model_attribute) { :size_less_than_or_equal_to } + let(:matcher_method) { :less_than_or_equal_to } + let(:validator_value) { 2.kilobytes } - it { is_expected_not_to_match_for(klass) } - end + include SizeValidatorMatcherTest::OnlyMatchWhenExactValue end describe '#greater_than' do let(:model_attribute) { :size_greater_than } + let(:matcher_method) { :greater_than } + let(:validator_value) { 7.kilobytes } - describe 'when provided with the exact size specified in the model validations' do - subject { matcher.greater_than 7.kilobytes } - - 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.greater_than 10.kilobytes } - - it { is_expected_not_to_match_for(klass) } - end - - describe 'when provided with a lower size than the size specified in the model validations' do - subject { matcher.greater_than 0.5.kilobyte } - - it { is_expected_not_to_match_for(klass) } - end + include SizeValidatorMatcherTest::OnlyMatchWhenExactValue end describe '#greater_than_or_equal_to' do let(:model_attribute) { :size_greater_than_or_equal_to } + let(:matcher_method) { :greater_than_or_equal_to } + let(:validator_value) { 7.kilobytes } - describe 'when provided with the exact size specified in the model validations' do - subject { matcher.greater_than_or_equal_to 7.kilobytes } - - 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.greater_than_or_equal_to 10.kilobytes } - - it { is_expected_not_to_match_for(klass) } - end - - describe 'when provided with a lower size than the size specified in the model validations' do - subject { matcher.greater_than_or_equal_to 0.5.kilobyte } - - it { is_expected_not_to_match_for(klass) } - end + include SizeValidatorMatcherTest::OnlyMatchWhenExactValue end describe '#between' do @@ -172,7 +142,7 @@ subject { matcher.less_than 2.kilobytes } let(:model_attribute) { :size_less_than } - let(:instance) { Size::Portfolio.new } + let(:instance) { klass.new } it { is_expected_to_match_for(instance) } end From d92cb2cdd72bac0e4e2e8bbd78ba236536dbb68b Mon Sep 17 00:00:00 2001 From: Mth0158 Date: Mon, 6 Nov 2023 16:59:40 +0100 Subject: [PATCH 08/13] [Test] Refacto attached_validator_matcher_test file --- test/dummy/app/models/attached.rb | 5 ++ test/dummy/app/models/attached/matcher.rb | 20 +++++ test/dummy/db/schema.rb | 5 ++ .../attached_validator_matcher_test.rb | 89 ++++++++----------- 4 files changed, 67 insertions(+), 52 deletions(-) create mode 100644 test/dummy/app/models/attached.rb create mode 100644 test/dummy/app/models/attached/matcher.rb 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/db/schema.rb b/test/dummy/db/schema.rb index 9bd5ef5a..19666dce 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -34,6 +34,11 @@ 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 :dimension_matchers, force: :cascade do |t| t.datetime :created_at, null: false t.datetime :updated_at, null: false diff --git a/test/matchers/attached_validator_matcher_test.rb b/test/matchers/attached_validator_matcher_test.rb index 689838cf..99f17015 100644 --- a/test/matchers/attached_validator_matcher_test.rb +++ b/test/matchers/attached_validator_matcher_test.rb @@ -1,70 +1,55 @@ # 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')) + it { is_expected_not_to_match_for(klass) } + end end - test 'negative match with invalid conditional validation' do - matcher = ActiveStorageValidations::Matchers::AttachedValidatorMatcher.new(:conditional_image) - refute matcher.matches?(User.new) - end + describe 'when the passed model attribute' do + describe 'does not exist' do + subject { matcher } - 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) + 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 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 From a1b5ad599f5d5526d8a8ff5f3eff45e65583d538 Mon Sep 17 00:00:00 2001 From: Mth0158 Date: Mon, 6 Nov 2023 17:46:47 +0100 Subject: [PATCH 09/13] [Test] Refacto content_type_validator_matcher test file --- .../content_type_validator_matcher.rb | 35 +-- test/dummy/app/models/content_type.rb | 5 + test/dummy/app/models/content_type/matcher.rb | 26 ++ test/dummy/db/schema.rb | 5 + .../content_type_validator_matcher_test.rb | 295 +++++++++++++----- 5 files changed, 274 insertions(+), 92 deletions(-) create mode 100644 test/dummy/app/models/content_type.rb create mode 100644 test/dummy/app/models/content_type/matcher.rb 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..819856c1 100644 --- a/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +++ b/lib/active_storage_validations/matchers/content_type_validator_matcher.rb @@ -11,6 +11,7 @@ def validate_content_type_of(name) class ContentTypeValidatorMatcher def initialize(attribute_name) @attribute_name = attribute_name + @allowed_types = @rejected_types = [] @custom_message = nil end @@ -35,20 +36,20 @@ 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,22 +63,14 @@ 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 || [] - 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 all_rejected_types_rejected? + @rejected_types_not_rejected ||= @rejected_types.select { |type| type_allowed?(type) } + @rejected_types_not_rejected.empty? end def type_allowed?(type) 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/db/schema.rb b/test/dummy/db/schema.rb index 19666dce..1f462ef2 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -39,6 +39,11 @@ 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 diff --git a/test/matchers/content_type_validator_matcher_test.rb b/test/matchers/content_type_validator_matcher_test.rb index ff45431a..638448fb 100644 --- a/test/matchers/content_type_validator_matcher_test.rb +++ b/test/matchers/content_type_validator_matcher_test.rb @@ -1,98 +1,251 @@ # 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) } - test 'positive match for rejecting' do - matcher = ActiveStorageValidations::Matchers::ContentTypeValidatorMatcher.new(:avatar) - matcher.rejecting('image/jpeg') - assert matcher.matches?(User) - end + 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' } - test 'negative match for rejecting' do - matcher = ActiveStorageValidations::Matchers::ContentTypeValidatorMatcher.new(:avatar) - matcher.rejecting('image/png') - refute matcher.matches?(User) + it { is_expected_not_to_match_for(klass) } + end + end + + 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 'positive match on subset of accepted content types' do - matcher = ActiveStorageValidations::Matchers::ContentTypeValidatorMatcher.new(:photos) - matcher.allowing('image/png') - assert 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 '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 '#with_message' do + let(:model_attribute) { :with_message } + + describe 'when provided with the model validation message' do + subject { matcher.with_message('Not authorized file type.') } + + it { is_expected_to_match_for(klass) } + end + + describe 'when provided with a different message than the model validation message' do + subject { matcher.with_message('') } + + it { is_expected_not_to_match_for(klass) } + 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 '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 - 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 '#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 + + describe 'when the passed model attribute does not exist' do + subject { matcher } + + 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.with_message('Not authorized file type.') } + + let(:model_attribute) { :with_message } + let(:instance) { klass.new } + + it { is_expected_to_match_for(instance) } + end end From 21ea321b4d28c293a066c1851c3b09575d4b1eb4 Mon Sep 17 00:00:00 2001 From: Mth0158 Date: Mon, 6 Nov 2023 19:01:21 +0100 Subject: [PATCH 10/13] [Test] Refacto size_validator_matcher_test and extend its tests --- test/dummy/app/models/size.rb | 5 + test/dummy/app/models/size/matcher.rb | 38 ++++++++ test/dummy/app/models/size/portfolio.rb | 2 +- .../app/models/size/several_validator.rb | 2 +- .../app/models/size/several_validator_proc.rb | 2 +- test/dummy/app/models/size/zero_validator.rb | 2 +- .../app/models/size/zero_validator_proc.rb | 2 +- test/dummy/db/schema.rb | 25 +++-- test/matchers/size_validator_matcher_test.rb | 97 +++++++++++++++++-- 9 files changed, 150 insertions(+), 25 deletions(-) create mode 100644 test/dummy/app/models/size.rb create mode 100644 test/dummy/app/models/size/matcher.rb 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..f0c680a7 --- /dev/null +++ b/test/dummy/app/models/size/matcher.rb @@ -0,0 +1,38 @@ +# 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 :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 1f462ef2..9be891c3 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -69,40 +69,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/size_validator_matcher_test.rb b/test/matchers/size_validator_matcher_test.rb index eadba2b7..3a77168b 100644 --- a/test/matchers/size_validator_matcher_test.rb +++ b/test/matchers/size_validator_matcher_test.rb @@ -34,42 +34,42 @@ module OnlyMatchWhenExactValue include MatcherHelpers let(:matcher) { ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(model_attribute) } - let(:klass) { Size::Portfolio } + let(:klass) { Size::Matcher } describe '#less_than' do - let(:model_attribute) { :size_less_than } let(:matcher_method) { :less_than } + let(:model_attribute) { matcher_method } let(:validator_value) { 2.kilobytes } include SizeValidatorMatcherTest::OnlyMatchWhenExactValue end describe '#less_than_or_equal_to' do - let(:model_attribute) { :size_less_than_or_equal_to } let(:matcher_method) { :less_than_or_equal_to } + let(:model_attribute) { matcher_method } let(:validator_value) { 2.kilobytes } include SizeValidatorMatcherTest::OnlyMatchWhenExactValue end describe '#greater_than' do - let(:model_attribute) { :size_greater_than } let(:matcher_method) { :greater_than } + let(:model_attribute) { matcher_method } let(:validator_value) { 7.kilobytes } include SizeValidatorMatcherTest::OnlyMatchWhenExactValue end describe '#greater_than_or_equal_to' do - let(:model_attribute) { :size_greater_than_or_equal_to } 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) { :size_between } + let(:model_attribute) { :between } describe 'when provided with the exact sizes specified in the model validations' do subject { matcher.between 2.kilobytes..7.kilobytes } @@ -113,12 +113,12 @@ module OnlyMatchWhenExactValue end describe '#with_message' do - before { subject.between 2.kilobytes..7.kilobytes } + before { subject.less_than_or_equal_to 5.megabytes } - let(:model_attribute) { :size_with_message } + let(:model_attribute) { :with_message } describe 'when provided with the model validation message' do - subject { matcher.with_message('is not in required file size range') } + subject { matcher.with_message('File is too big.') } it { is_expected_to_match_for(klass) } end @@ -130,6 +130,83 @@ module OnlyMatchWhenExactValue 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 + + it { is_expected_to_match_for(klass) } + end + end + end + + 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 + + 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 + + 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 + + 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 does not exist' do subject { matcher.less_than 2.kilobytes } @@ -141,7 +218,7 @@ module OnlyMatchWhenExactValue describe 'when the matcher is provided with an instance' do subject { matcher.less_than 2.kilobytes } - let(:model_attribute) { :size_less_than } + let(:model_attribute) { :less_than } let(:instance) { klass.new } it { is_expected_to_match_for(instance) } From f1dd209aa5744c38a97d87d931e44c6972022882 Mon Sep 17 00:00:00 2001 From: Mth0158 Date: Mon, 6 Nov 2023 19:41:30 +0100 Subject: [PATCH 11/13] [Test] Add proc validator tests to size_validator_matcher --- test/dummy/app/models/size/matcher.rb | 11 +++++ test/matchers/size_validator_matcher_test.rb | 46 +++++++++++++++----- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/test/dummy/app/models/size/matcher.rb b/test/dummy/app/models/size/matcher.rb index f0c680a7..8f8e8b0a 100644 --- a/test/dummy/app/models/size/matcher.rb +++ b/test/dummy/app/models/size/matcher.rb @@ -21,6 +21,17 @@ class Size::Matcher < ApplicationRecord 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.' } diff --git a/test/matchers/size_validator_matcher_test.rb b/test/matchers/size_validator_matcher_test.rb index 3a77168b..d3e164a7 100644 --- a/test/matchers/size_validator_matcher_test.rb +++ b/test/matchers/size_validator_matcher_test.rb @@ -9,22 +9,46 @@ module OnlyMatchWhenExactValue extend ActiveSupport::Concern included 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) } + 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 - describe 'when provided with the exact size specified in the model validations' do - subject { matcher.public_send(matcher_method, validator_value) } + describe 'proc validator' do + let(:matcher) { ActiveStorageValidations::Matchers::SizeValidatorMatcher.new(:"proc_#{model_attribute}") } - it { is_expected_to_match_for(klass) } - end + 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) } - 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 - it { is_expected_not_to_match_for(klass) } + 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 end end From 9872d4cd1e1868ca600f7d7552a1e017d295a2fd Mon Sep 17 00:00:00 2001 From: Mth0158 Date: Mon, 6 Nov 2023 20:08:13 +0100 Subject: [PATCH 12/13] [Test] Add our first matcher integration test --- test/dummy/app/models/integration.rb | 5 +++ test/dummy/app/models/integration/matcher.rb | 16 +++++++ test/dummy/db/schema.rb | 5 +++ test/matchers/integration/integration_test.rb | 44 +++++++++++++++++++ 4 files changed, 70 insertions(+) create mode 100644 test/dummy/app/models/integration.rb create mode 100644 test/dummy/app/models/integration/matcher.rb create mode 100644 test/matchers/integration/integration_test.rb 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/db/schema.rb b/test/dummy/db/schema.rb index 9be891c3..bf1a8359 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -54,6 +54,11 @@ 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 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 From c82a8f9ef820de61ae412fec6b5932512c19f7bd Mon Sep 17 00:00:00 2001 From: Mth0158 Date: Mon, 6 Nov 2023 21:28:01 +0100 Subject: [PATCH 13/13] [Validator/Matcher] Fix the error logic with custom_message & validation refacto (#203) --- .../aspect_ratio_validator.rb | 2 + .../attached_validator.rb | 7 ++- .../concerns/symbolizable.rb | 10 ++++ .../content_type_validator.rb | 8 ++- .../dimension_validator.rb | 15 +++++ .../error_handler.rb | 15 +++-- .../limit_validator.rb | 3 + .../matchers/attached_validator_matcher.rb | 54 ++++++++++++----- .../matchers/concerns/validatable.rb | 46 +++++++++++++++ .../content_type_validator_matcher.rb | 40 +++++++++---- .../matchers/dimension_validator_matcher.rb | 56 ++++++++++++++---- .../matchers/size_validator_matcher.rb | 59 +++++++++++++++---- .../processable_image_validator.rb | 2 + .../size_validator.rb | 20 ++++++- test/active_storage_validations_test.rb | 4 +- .../attached_validator_matcher_test.rb | 16 +++++ .../content_type_validator_matcher_test.rb | 44 ++++++++++---- .../dimension_validator_matcher_test.rb | 22 +++++-- test/matchers/size_validator_matcher_test.rb | 36 ++++++++--- 19 files changed, 372 insertions(+), 87 deletions(-) create mode 100644 lib/active_storage_validations/concerns/symbolizable.rb create mode 100644 lib/active_storage_validations/matchers/concerns/validatable.rb 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 819856c1..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,6 +12,8 @@ def validate_content_type_of(name) end class ContentTypeValidatorMatcher + include Validatable + def initialize(attribute_name) @attribute_name = attribute_name @allowed_types = @rejected_types = [] @@ -36,7 +41,11 @@ def with_message(message) def matches?(subject) @subject = subject.is_a?(Class) ? subject.new : subject - responds_to_methods && all_allowed_types_allowed? && all_rejected_types_rejected? && validate_custom_message? + + responds_to_methods && + all_allowed_types_allowed? && + all_rejected_types_rejected? && + validate_custom_message? end def failure_message @@ -74,30 +83,35 @@ def all_rejected_types_rejected? end def type_allowed?(type) + attach_file_of_type(type) + validate + is_valid? + end + + 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/matchers/attached_validator_matcher_test.rb b/test/matchers/attached_validator_matcher_test.rb index 99f17015..d63f8784 100644 --- a/test/matchers/attached_validator_matcher_test.rb +++ b/test/matchers/attached_validator_matcher_test.rb @@ -24,6 +24,12 @@ it { is_expected_not_to_match_for(klass) } end + + describe 'when not provided with the #with_message matcher method' do + subject { matcher } + + it { is_expected_to_match_for(klass) } + end end describe 'when the passed model attribute' do @@ -42,6 +48,16 @@ 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 describe 'when the matcher is provided with an instance' do diff --git a/test/matchers/content_type_validator_matcher_test.rb b/test/matchers/content_type_validator_matcher_test.rb index 638448fb..89673ba6 100644 --- a/test/matchers/content_type_validator_matcher_test.rb +++ b/test/matchers/content_type_validator_matcher_test.rb @@ -148,16 +148,26 @@ describe '#with_message' do let(:model_attribute) { :with_message } - describe 'when provided with the model validation message' do - subject { matcher.with_message('Not authorized file type.') } + describe 'when provided with the allowed content type' do + before { matcher.allowing('image/png') } - it { is_expected_to_match_for(klass) } - end + describe 'and with the message specified in the model validations' do + subject { matcher.with_message('Not authorized file type.') } - describe 'when provided with a different message than the model validation message' do - subject { matcher.with_message('') } + it { is_expected_to_match_for(klass) } + end - it { is_expected_not_to_match_for(klass) } + 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 @@ -232,12 +242,24 @@ end end - describe 'when the passed model attribute does not exist' do - subject { matcher } + 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 'has a custom validation error message' do + describe 'but the matcher is not provided with a #with_message' do + subject { matcher } - let(:model_attribute) { :not_present_in_model } + let(:model_attribute) { :with_message } - it { is_expected_not_to_match_for(klass) } + it { is_expected_to_match_for(klass) } + end + end end describe 'when the matcher is provided with an instance' do diff --git a/test/matchers/dimension_validator_matcher_test.rb b/test/matchers/dimension_validator_matcher_test.rb index 96f87454..87ba7ef2 100644 --- a/test/matchers/dimension_validator_matcher_test.rb +++ b/test/matchers/dimension_validator_matcher_test.rb @@ -346,7 +346,7 @@ module OnlyMatchWhenExactValues matcher.height(150) end - it { is_expected_not_to_match_for(klass) } + it { is_expected_to_match_for(klass) } end end end @@ -564,12 +564,24 @@ module OnlyMatchWhenExactValues end end - describe 'when the passed model attribute does not exist' do - subject { matcher.width(150) } + 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) { :not_present_in_model } + let(:model_attribute) { :width_exact_with_message } - it { is_expected_not_to_match_for(klass) } + it { is_expected_to_match_for(klass) } + end + end end describe 'when the matcher is provided with an instance' do diff --git a/test/matchers/size_validator_matcher_test.rb b/test/matchers/size_validator_matcher_test.rb index d3e164a7..c80d1317 100644 --- a/test/matchers/size_validator_matcher_test.rb +++ b/test/matchers/size_validator_matcher_test.rb @@ -137,20 +137,28 @@ module OnlyMatchWhenExactValue end describe '#with_message' do - before { subject.less_than_or_equal_to 5.megabytes } - let(:model_attribute) { :with_message } - describe 'when provided with the model validation message' do - subject { matcher.with_message('File is too big.') } + describe 'when provided with the exact size' do + before { matcher.less_than_or_equal_to(5.megabytes) } - it { is_expected_to_match_for(klass) } - end + describe 'and with the message specified in the model validations' do + subject { matcher.with_message('File is too big.') } - describe 'when provided with a different message than the model validation message' do - subject { matcher.with_message('') } + it { is_expected_to_match_for(klass) } + end - it { is_expected_not_to_match_for(klass) } + 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 @@ -231,6 +239,16 @@ module OnlyMatchWhenExactValue 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 } + + let(:model_attribute) { :with_message } + + 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 }