active_storage_validations 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -22
  3. data/config/locales/da.yml +1 -2
  4. data/config/locales/de.yml +1 -1
  5. data/config/locales/en.yml +1 -1
  6. data/config/locales/es.yml +1 -1
  7. data/config/locales/fr.yml +1 -1
  8. data/config/locales/it.yml +1 -1
  9. data/config/locales/ja.yml +1 -1
  10. data/config/locales/nl.yml +1 -1
  11. data/config/locales/pl.yml +1 -1
  12. data/config/locales/pt-BR.yml +1 -1
  13. data/config/locales/ru.yml +1 -1
  14. data/config/locales/sv.yml +1 -1
  15. data/config/locales/tr.yml +1 -1
  16. data/config/locales/uk.yml +1 -1
  17. data/config/locales/vi.yml +1 -1
  18. data/config/locales/zh-CN.yml +1 -1
  19. data/lib/active_storage_validations/aspect_ratio_validator.rb +57 -66
  20. data/lib/active_storage_validations/attached_validator.rb +5 -3
  21. data/lib/active_storage_validations/base_size_validator.rb +4 -1
  22. data/lib/active_storage_validations/concerns/active_storageable.rb +28 -0
  23. data/lib/active_storage_validations/concerns/attachable.rb +134 -0
  24. data/lib/active_storage_validations/concerns/errorable.rb +4 -4
  25. data/lib/active_storage_validations/concerns/loggable.rb +9 -0
  26. data/lib/active_storage_validations/concerns/optionable.rb +27 -0
  27. data/lib/active_storage_validations/content_type_spoof_detector.rb +94 -0
  28. data/lib/active_storage_validations/content_type_validator.rb +113 -39
  29. data/lib/active_storage_validations/dimension_validator.rb +32 -52
  30. data/lib/active_storage_validations/limit_validator.rb +8 -5
  31. data/lib/active_storage_validations/marcel_extensor.rb +5 -0
  32. data/lib/active_storage_validations/matchers/concerns/attachable.rb +27 -9
  33. data/lib/active_storage_validations/matchers/concerns/messageable.rb +1 -1
  34. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +7 -0
  35. data/lib/active_storage_validations/matchers/limit_validator_matcher.rb +127 -0
  36. data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +1 -10
  37. data/lib/active_storage_validations/matchers.rb +4 -15
  38. data/lib/active_storage_validations/metadata.rb +22 -26
  39. data/lib/active_storage_validations/processable_image_validator.rb +17 -32
  40. data/lib/active_storage_validations/size_validator.rb +3 -7
  41. data/lib/active_storage_validations/total_size_validator.rb +4 -8
  42. data/lib/active_storage_validations/version.rb +1 -1
  43. data/lib/active_storage_validations.rb +2 -1
  44. metadata +67 -21
  45. data/lib/active_storage_validations/option_proc_unfolding.rb +0 -16
@@ -8,6 +8,27 @@ module ActiveStorageValidations
8
8
  @subject.public_send(@attribute_name)
9
9
  end
10
10
 
11
+ def attach_files(count)
12
+ return unless count.positive?
13
+
14
+ file_array = Array.new(count, dummy_file)
15
+
16
+ @subject.public_send(@attribute_name).attach(file_array)
17
+ end
18
+
19
+ def detach_file
20
+ @subject.attachment_changes.delete(@attribute_name.to_s)
21
+ end
22
+ alias :detach_files :detach_file
23
+
24
+ def file_attached?
25
+ @subject.public_send(@attribute_name).attached?
26
+ end
27
+
28
+ def dummy_blob
29
+ ActiveStorage::Blob.create_and_upload!(**dummy_file)
30
+ end
31
+
11
32
  def dummy_file
12
33
  {
13
34
  io: io,
@@ -18,8 +39,8 @@ module ActiveStorageValidations
18
39
 
19
40
  def processable_image
20
41
  {
21
- io: File.open(Rails.root.join('public', 'image_1920x1080.png')),
22
- filename: 'image_1920x1080_file.png',
42
+ io: StringIO.new(image_data),
43
+ filename: 'processable_image.png',
23
44
  content_type: 'image/png'
24
45
  }
25
46
  end
@@ -27,7 +48,7 @@ module ActiveStorageValidations
27
48
  def not_processable_image
28
49
  {
29
50
  io: Tempfile.new('.'),
30
- filename: 'processable.txt',
51
+ filename: 'not_processable_image.txt',
31
52
  content_type: 'text/plain'
32
53
  }
33
54
  end
@@ -36,12 +57,9 @@ module ActiveStorageValidations
36
57
  @io ||= Tempfile.new('Hello world!')
37
58
  end
38
59
 
39
- def detach_file
40
- @subject.attachment_changes.delete(@attribute_name.to_s)
41
- end
42
-
43
- def file_attached?
44
- @subject.public_send(@attribute_name).attached?
60
+ def image_data
61
+ # Binary data for a 1x1 transparent PNG image
62
+ "\x89PNG\r\n\x1A\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1F\x15\xC4\x89\x00\x00\x00\nIDATx\x9Cc\x00\x01\x00\x00\x05\x00\x01\r\n\x2D\xB4\x00\x00\x00\x00IEND\xAE\x42\x60\x82"
45
63
  end
46
64
  end
47
65
  end
@@ -18,7 +18,7 @@ module ActiveStorageValidations
18
18
 
19
19
  def has_an_error_message_which_is_custom_message?
20
20
  validator_errors_for_attribute.one? do |error|
21
- error[:error] == @custom_message
21
+ error[:custom_message] == @custom_message
22
22
  end
23
23
  end
24
24
  end
@@ -130,6 +130,13 @@ module ActiveStorageValidations
130
130
  content_type: type
131
131
  }
132
132
  end
133
+
134
+ # Due to the way we build test attachments in #attachment_for
135
+ # (ie spoofed file basically), we need to ignore the error related to
136
+ # content type spoofing in our matcher to pass the tests
137
+ def validator_errors_for_attribute
138
+ super.reject { |hash| hash[:error] == :spoofed_content_type }
139
+ end
133
140
  end
134
141
  end
135
142
  end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'concerns/active_storageable'
4
+ require_relative 'concerns/allow_blankable'
5
+ require_relative 'concerns/attachable'
6
+ require_relative 'concerns/contextable'
7
+ require_relative 'concerns/messageable'
8
+ require_relative 'concerns/rspecable'
9
+ require_relative 'concerns/validatable'
10
+
11
+ module ActiveStorageValidations
12
+ module Matchers
13
+ def validate_limits_of(name)
14
+ LimitValidatorMatcher.new(name)
15
+ end
16
+
17
+ class LimitValidatorMatcher
18
+ include ActiveStorageable
19
+ include AllowBlankable
20
+ include Attachable
21
+ include Contextable
22
+ include Messageable
23
+ include Rspecable
24
+ include Validatable
25
+
26
+ def initialize(attribute_name)
27
+ initialize_allow_blankable
28
+ initialize_contextable
29
+ initialize_messageable
30
+ initialize_rspecable
31
+ @attribute_name = attribute_name
32
+ @min = @max = nil
33
+ end
34
+
35
+ def description
36
+ "validate the limit files of :#{@attribute_name}"
37
+ end
38
+
39
+ def failure_message
40
+ message = ["is expected to validate limit file of :#{@attribute_name}"]
41
+ build_failure_message(message)
42
+ message.join("\n")
43
+ end
44
+
45
+ def min(count)
46
+ @min = count
47
+ self
48
+ end
49
+
50
+ def max(count)
51
+ @max = count
52
+ self
53
+ end
54
+
55
+ def matches?(subject)
56
+ @subject = subject.is_a?(Class) ? subject.new : subject
57
+
58
+ is_a_valid_active_storage_attribute? &&
59
+ is_context_valid? &&
60
+ is_custom_message_valid? &&
61
+ file_count_not_smaller_than_min? &&
62
+ file_count_equal_min? &&
63
+ file_count_larger_than_min? &&
64
+ file_count_smaller_than_max? &&
65
+ file_count_equal_max? &&
66
+ file_count_not_larger_than_max?
67
+ end
68
+
69
+ private
70
+
71
+ def build_failure_message(message)
72
+ return unless @failure_message_artefacts.present?
73
+
74
+ message << " but there seem to have issues with the matcher methods you used, since:"
75
+ @failure_message_artefacts.each do |error_case|
76
+ message << " validation failed when provided with #{error_case[:count]} file(s)"
77
+ end
78
+ message << " whereas it should have passed"
79
+ end
80
+
81
+ def file_count_not_smaller_than_min?
82
+ @min.nil? || @min.zero? || !passes_validation_with_limits(@min - 1)
83
+ end
84
+
85
+ def file_count_equal_min?
86
+ @min.nil? || @min.zero? || passes_validation_with_limits(@min)
87
+ end
88
+
89
+ def file_count_larger_than_min?
90
+ @min.nil? || @min.zero? || @min == @max || passes_validation_with_limits(@min + 1)
91
+ end
92
+
93
+ def file_count_smaller_than_max?
94
+ @max.nil? || @min == @max || passes_validation_with_limits(@max - 1)
95
+ end
96
+
97
+ def file_count_equal_max?
98
+ @max.nil? || passes_validation_with_limits(@max)
99
+ end
100
+
101
+ def file_count_not_larger_than_max?
102
+ @max.nil? || !passes_validation_with_limits(@max + 1)
103
+ end
104
+
105
+ def passes_validation_with_limits(count)
106
+ attach_files(count)
107
+ validate
108
+ detach_files
109
+ is_valid? || add_failure_message_artefact(count)
110
+ end
111
+
112
+ def is_custom_message_valid?
113
+ return true if !@custom_message || (@min&.zero? && @max.nil?)
114
+
115
+ @min.nil? ? attach_files(@max + 1) : attach_files(@min - 1)
116
+ validate
117
+ detach_files
118
+ has_an_error_message_which_is_custom_message?
119
+ end
120
+
121
+ def add_failure_message_artefact(count)
122
+ @failure_message_artefacts << { count: count }
123
+ false
124
+ end
125
+ end
126
+ end
127
+ end
@@ -22,19 +22,10 @@ module ActiveStorageValidations
22
22
  protected
23
23
 
24
24
  def attach_file
25
- # We attach blobs instead of io for has_many_attached relation
25
+ # has_many_attached relation
26
26
  @subject.public_send(@attribute_name).attach([dummy_blob])
27
27
  @subject.public_send(@attribute_name)
28
28
  end
29
-
30
- def dummy_blob
31
- ActiveStorage::Blob.create_and_upload!(
32
- io: io,
33
- filename: 'test.png',
34
- content_type: 'image/png',
35
- service_name: 'test'
36
- )
37
- end
38
29
  end
39
30
  end
40
31
  end
@@ -3,6 +3,7 @@
3
3
  require 'active_storage_validations/matchers/aspect_ratio_validator_matcher'
4
4
  require 'active_storage_validations/matchers/attached_validator_matcher'
5
5
  require 'active_storage_validations/matchers/processable_image_validator_matcher'
6
+ require 'active_storage_validations/matchers/limit_validator_matcher'
6
7
  require 'active_storage_validations/matchers/content_type_validator_matcher'
7
8
  require 'active_storage_validations/matchers/dimension_validator_matcher'
8
9
  require 'active_storage_validations/matchers/size_validator_matcher'
@@ -25,21 +26,9 @@ module ActiveStorageValidations
25
26
  end
26
27
 
27
28
  def self.mock_metadata(attachment, width, height)
28
- if Rails.gem_version >= Gem::Version.new('6.0.0')
29
- # Mock the Metadata class for rails 6
30
- mock = OpenStruct.new(metadata: { width: width, height: height })
31
- stub_method(ActiveStorageValidations::Metadata, :new, mock) do
32
- yield
33
- end
34
- else
35
- # Stub the metadata analysis for rails 5
36
- stub_method(attachment, :analyze, true) do
37
- stub_method(attachment, :analyzed?, true) do
38
- stub_method(attachment, :metadata, { width: width, height: height }) do
39
- yield
40
- end
41
- end
42
- end
29
+ mock = Struct.new(:metadata).new({ width: width, height: height })
30
+ stub_method(ActiveStorageValidations::Metadata, :new, mock) do
31
+ yield
43
32
  end
44
33
  end
45
34
  end
@@ -1,14 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'concerns/loggable'
4
+
1
5
  module ActiveStorageValidations
2
6
  class Metadata
7
+ include Loggable
8
+
3
9
  class InvalidImageError < StandardError; end
4
10
 
5
- attr_reader :file
11
+ attr_reader :attachable
6
12
 
7
13
  DEFAULT_IMAGE_PROCESSOR = :mini_magick.freeze
8
14
 
9
- def initialize(file)
15
+ def initialize(attachable)
10
16
  require_image_processor
11
- @file = file
17
+ @attachable = attachable
12
18
  end
13
19
 
14
20
  def valid?
@@ -58,18 +64,9 @@ module ActiveStorageValidations
58
64
  end
59
65
 
60
66
  def read_image
61
- is_string = file.is_a?(String)
62
- if is_string || file.is_a?(ActiveStorage::Blob)
63
- blob =
64
- if is_string
65
- if Rails.gem_version < Gem::Version.new('6.1.0')
66
- ActiveStorage::Blob.find_signed(file)
67
- else
68
- ActiveStorage::Blob.find_signed!(file)
69
- end
70
- else
71
- file
72
- end
67
+ is_string = attachable.is_a?(String)
68
+ if is_string || attachable.is_a?(ActiveStorage::Blob)
69
+ blob = is_string ? ActiveStorage::Blob.find_signed!(attachable) : attachable
73
70
 
74
71
  tempfile = Tempfile.new(["ActiveStorage-#{blob.id}-", blob.filename.extension_with_delimiter])
75
72
  tempfile.binmode
@@ -105,9 +102,9 @@ module ActiveStorageValidations
105
102
  begin
106
103
  Vips::Image.new_from_file(path)
107
104
  rescue exception_class
108
- # We handle cases where an error is raised when reading the file
105
+ # We handle cases where an error is raised when reading the attachable
109
106
  # because Vips can throw errors rather than returning false
110
- # We stumble upon this issue while reading 0 byte size file
107
+ # We stumble upon this issue while reading 0 byte size attachable
111
108
  # https://github.com/janko/image_processing/issues/97
112
109
  false
113
110
  end
@@ -154,13 +151,13 @@ module ActiveStorageValidations
154
151
  end
155
152
 
156
153
  def read_file_path
157
- case file
154
+ case attachable
158
155
  when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
159
- file.path
156
+ attachable.path
160
157
  when Hash
161
- io = file.fetch(:io)
158
+ io = attachable.fetch(:io)
162
159
  if io.is_a?(StringIO)
163
- tempfile = Tempfile.new([File.basename(file[:filename], '.*'), File.extname(file[:filename])])
160
+ tempfile = Tempfile.new([File.basename(attachable[:filename], '.*'), File.extname(attachable[:filename])])
164
161
  tempfile.binmode
165
162
  IO.copy_stream(io, tempfile)
166
163
  io.rewind
@@ -170,14 +167,13 @@ module ActiveStorageValidations
170
167
  else
171
168
  File.open(io).path
172
169
  end
170
+ when File
171
+ attachable.path
172
+ when Pathname
173
+ attachable.to_s
173
174
  else
174
175
  raise "Something wrong with params."
175
176
  end
176
177
  end
177
-
178
- def logger
179
- Rails.logger
180
- end
181
-
182
178
  end
183
179
  end
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/active_storageable.rb'
4
+ require_relative 'concerns/attachable.rb'
3
5
  require_relative 'concerns/errorable.rb'
4
6
  require_relative 'concerns/symbolizable.rb'
5
- require_relative 'metadata.rb'
6
7
 
7
8
  module ActiveStorageValidations
8
9
  class ProcessableImageValidator < ActiveModel::EachValidator # :nodoc
9
- include OptionProcUnfolding
10
+ include ActiveStorageable
11
+ include Attachable
10
12
  include Errorable
11
13
  include Symbolizable
12
14
 
@@ -14,36 +16,19 @@ module ActiveStorageValidations
14
16
  image_not_processable
15
17
  ].freeze
16
18
 
17
- if Rails.gem_version >= Gem::Version.new('6.0.0')
18
- def validate_each(record, attribute, _value)
19
- return true unless record.send(attribute).attached?
20
-
21
- changes = record.attachment_changes[attribute.to_s]
22
- return true if changes.blank?
23
-
24
- files = Array.wrap(changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable)
25
-
26
- files.each do |file|
27
- if !Metadata.new(file).valid?
28
- errors_options = initialize_error_options(options, file)
29
- add_error(record, attribute, ERROR_TYPES.first , **errors_options) unless Metadata.new(file).valid?
30
- end
31
- end
32
- end
33
- else
34
- # Rails 5
35
- def validate_each(record, attribute, _value)
36
- return true unless record.send(attribute).attached?
37
-
38
- files = Array.wrap(record.send(attribute))
39
-
40
- files.each do |file|
41
- if !Metadata.new(file).valid?
42
- errors_options = initialize_error_options(options, file)
43
- add_error(record, attribute, ERROR_TYPES.first , **errors_options) unless Metadata.new(file).valid?
44
- end
45
- end
46
- end
19
+ def validate_each(record, attribute, _value)
20
+ return if no_attachments?(record, attribute)
21
+
22
+ validate_changed_files_from_metadata(record, attribute)
23
+ end
24
+
25
+ private
26
+
27
+ def is_valid?(record, attribute, attachable, metadata)
28
+ return if !metadata.empty?
29
+
30
+ errors_options = initialize_error_options(options, attachable)
31
+ add_error(record, attribute, ERROR_TYPES.first , **errors_options)
47
32
  end
48
33
  end
49
34
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'concerns/errorable.rb'
4
- require_relative 'concerns/symbolizable.rb'
5
3
  require_relative 'base_size_validator.rb'
6
4
 
7
5
  module ActiveStorageValidations
@@ -15,12 +13,11 @@ module ActiveStorageValidations
15
13
  ].freeze
16
14
 
17
15
  def validate_each(record, attribute, _value)
18
- return true unless record.send(attribute).attached?
16
+ return if no_attachments?(record, attribute)
19
17
 
20
- files = Array.wrap(record.send(attribute))
21
- flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
18
+ flat_options = set_flat_options(record)
22
19
 
23
- files.each do |file|
20
+ attached_files(record, attribute).each do |file|
24
21
  next if is_valid?(file.blob.byte_size, flat_options)
25
22
 
26
23
  errors_options = initialize_error_options(options, file)
@@ -31,7 +28,6 @@ module ActiveStorageValidations
31
28
  error_type = "file_size_not_#{keys.first}".to_sym
32
29
 
33
30
  add_error(record, attribute, error_type, **errors_options)
34
- break
35
31
  end
36
32
  end
37
33
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'concerns/errorable.rb'
4
- require_relative 'concerns/symbolizable.rb'
5
3
  require_relative 'base_size_validator.rb'
6
4
 
7
5
  module ActiveStorageValidations
@@ -17,14 +15,12 @@ module ActiveStorageValidations
17
15
  def validate_each(record, attribute, _value)
18
16
  custom_check_validity!(record, attribute)
19
17
 
20
- return true unless record.send(attribute).attached?
18
+ return if no_attachments?(record, attribute)
21
19
 
22
- files = Array.wrap(record.send(attribute))
23
- flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
20
+ total_file_size = attached_files(record, attribute).sum { |file| file.blob.byte_size }
21
+ flat_options = set_flat_options(record)
24
22
 
25
- total_file_size = files.sum { |file| file.blob.byte_size }
26
-
27
- return true if is_valid?(total_file_size, flat_options)
23
+ return if is_valid?(total_file_size, flat_options)
28
24
 
29
25
  errors_options = initialize_error_options(options, nil)
30
26
  populate_error_options(errors_options, flat_options)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorageValidations
4
- VERSION = '1.2.0'
4
+ VERSION = '1.3.1'
5
5
  end
@@ -5,7 +5,6 @@ require 'active_support/concern'
5
5
 
6
6
  require 'active_storage_validations/railtie'
7
7
  require 'active_storage_validations/engine'
8
- require 'active_storage_validations/option_proc_unfolding'
9
8
  require 'active_storage_validations/attached_validator'
10
9
  require 'active_storage_validations/content_type_validator'
11
10
  require 'active_storage_validations/limit_validator'
@@ -15,6 +14,8 @@ require 'active_storage_validations/processable_image_validator'
15
14
  require 'active_storage_validations/size_validator'
16
15
  require 'active_storage_validations/total_size_validator'
17
16
 
17
+ require 'active_storage_validations/marcel_extensor'
18
+
18
19
  ActiveSupport.on_load(:active_record) do
19
20
  send :include, ActiveStorageValidations
20
21
  end