active_storage_validations 0.8.6 → 0.8.7

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 354dd29a7d41df8c36694ecb4424869a5a6c0c7add98a32186995199c036ae23
4
- data.tar.gz: db75c2027d7e9fe55150223007b414957c67bdf1d3ccd5a58fcbb38daf836e1a
3
+ metadata.gz: 8ee7d6623af014086158c960b7ef7408f4cf7527536f57539765351357160189
4
+ data.tar.gz: 6819ee596bd09560ca801fade1006bfbe4bf03b820b7ca1b0f84eafe0bb47ac1
5
5
  SHA512:
6
- metadata.gz: c919243c4a4791c4615d5b3112c896462ab34125ed3b4974a0ad1d29bf53b8521a95dc5c34636ad11025f607247794f2c29772217f2fc130771493330b569bd1
7
- data.tar.gz: 9ecc389a981df18f6663e0ae112df499e9bf56e5f0f99a7a086c3cf59368625bc530e4f0c5add8d25f1e783ab8727aaa34bdf0ec3ca197be7035e3931a93fd44
6
+ metadata.gz: c1a79fa7e8825320b308f8a689b3b09763072ec8093c5fc1f43e4686dad3dd05b4d345ce8d62149b23865b5c215b90dcdd68b292ea4ccef26456e752a35aad5a
7
+ data.tar.gz: 69517cf6f0d5a5c64c39416945f06bb31ccb6c5b1c0288d883005184c96e353d44a3f9479767783bd9aae856ee2157a6ffdde48ebdb6abbf23a5f4972fd8f3b0
data/README.md CHANGED
@@ -189,6 +189,92 @@ Very simple example of validation with file attached, content type check and cus
189
189
 
190
190
  [![Sample](https://raw.githubusercontent.com/igorkasyanchuk/active_storage_validations/master/docs/preview.png)](https://raw.githubusercontent.com/igorkasyanchuk/active_storage_validations/master/docs/preview.png)
191
191
 
192
+ ## Test matchers
193
+ Provides RSpec-compatible and Minitest-compatible matchers for testing the validators.
194
+
195
+ ### RSpec
196
+
197
+ In spec_helper.rb, you'll need to require the matchers:
198
+
199
+ ```ruby
200
+ require 'active_storage_validations/matchers'
201
+ ```
202
+
203
+ And _include_ the module:
204
+
205
+ ```ruby
206
+ RSpec.configure do |config|
207
+ config.include ActiveStorageValidations::Matchers
208
+ end
209
+ ```
210
+
211
+ Example (Note that the options are chainable):
212
+
213
+ ```ruby
214
+ describe User do
215
+ it { is_expected.to validate_attached_of(:avatar) }
216
+
217
+ it { is_expected.to validate_content_type_of(:avatar).allowing('image/png', 'image/gif') }
218
+ it { is_expected.to validate_content_type_of(:avatar).rejecting('text/plain', 'text/xml') }
219
+
220
+ it { is_expected.to validate_dimensions_of(:avatar).width(250) }
221
+ it { is_expected.to validate_dimensions_of(:avatar).height(200) }
222
+ it { is_expected.to validate_dimensions_of(:avatar).width_min(200) }
223
+ it { is_expected.to validate_dimensions_of(:avatar).width_max(500) }
224
+ it { is_expected.to validate_dimensions_of(:avatar).height_min(100) }
225
+ it { is_expected.to validate_dimensions_of(:avatar).height_max(300) }
226
+ it { is_expected.to validate_dimensions_of(:avatar).width_between(200..500) }
227
+ it { is_expected.to validate_dimensions_of(:avatar).height_between(100..300) }
228
+
229
+ it { is_expected.to validate_size_of(:avatar).less_than(50.kilobytes) }
230
+ it { is_expected.to validate_size_of(:avatar).less_than_or_equal_to(50.kilobytes) }
231
+ it { is_expected.to validate_size_of(:avatar).greater_than(1.kilobyte) }
232
+ it { is_expected.to validate_size_of(:avatar).greater_than_or_equal_to(1.kilobyte) }
233
+ it { is_expected.to validate_size_of(:avatar).between(100..500.kilobytes) }
234
+ end
235
+ ```
236
+
237
+ ### Minitest
238
+ To use the following syntax, make sure you have the [shoulda-context](https://github.com/thoughtbot/shoulda-context) gem up and running. To make use of the matchers you need to require the matchers:
239
+
240
+ ```ruby
241
+ require 'active_storage_validations/matchers'
242
+ ```
243
+
244
+ And _extend_ the module:
245
+
246
+ ```bash
247
+ class ActiveSupport::TestCase
248
+ extend ActiveStorageValidations::Matchers
249
+ end
250
+ ```
251
+
252
+ Example (Note that the options are chainable):
253
+
254
+ ```ruby
255
+ class UserTest < ActiveSupport::TestCase
256
+ should validate_attached_of(:avatar)
257
+
258
+ should validate_content_type_of(:avatar).allowing('image/png', 'image/gif')
259
+ should validate_content_type_of(:avatar).rejecting('text/plain', 'text/xml')
260
+
261
+ should validate_dimensions_of(:avatar).width(250)
262
+ should validate_dimensions_of(:avatar).height(200)
263
+ should validate_dimensions_of(:avatar).width_min(200)
264
+ should validate_dimensions_of(:avatar).width_max(500)
265
+ should validate_dimensions_of(:avatar).height_min(100)
266
+ should validate_dimensions_of(:avatar).height_max(300)
267
+ should validate_dimensions_of(:avatar).width_between(200..500)
268
+ should validate_dimensions_of(:avatar).height_between(100..300)
269
+
270
+ should validate_size_of(:avatar).less_than(50.kilobytes)
271
+ should validate_size_of(:avatar).less_than_or_equal_to(50.kilobytes)
272
+ should validate_size_of(:avatar).greater_than(1.kilobyte)
273
+ should validate_size_of(:avatar).greater_than_or_equal_to(1.kilobyte)
274
+ should validate_size_of(:avatar).between(100..500.kilobytes)
275
+ end
276
+ ```
277
+
192
278
  ## Todo
193
279
 
194
280
  * verify with remote storages (s3, etc)
@@ -240,6 +326,7 @@ You are welcome to contribute.
240
326
  - https://github.com/UICJohn
241
327
  - https://github.com/giovannibonetti
242
328
  - https://github.com/dlepage
329
+ - https://github.com/StefSchenkelaars
243
330
 
244
331
  ## License
245
332
 
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_storage_validations/matchers/attached_validator_matcher'
4
+ require 'active_storage_validations/matchers/content_type_validator_matcher'
5
+ require 'active_storage_validations/matchers/dimension_validator_matcher'
6
+ require 'active_storage_validations/matchers/size_validator_matcher'
7
+
8
+ module ActiveStorageValidations
9
+ module Matchers
10
+ # Helper to stub a method with either RSpec or Minitest (whatever is available)
11
+ def self.stub_method(object, method, result)
12
+ if defined?(Minitest::Mock)
13
+ object.stub(method, result) do
14
+ yield
15
+ end
16
+ elsif defined?(RSpec::Mocks)
17
+ RSpec::Mocks.allow_message(object, method) { result }
18
+ yield
19
+ else
20
+ raise 'Need either Minitest::Mock or RSpec::Mocks to run this validator matcher'
21
+ end
22
+ end
23
+
24
+ def self.mock_metadata(attachment, width, height)
25
+ if Rails::VERSION::MAJOR >= 6
26
+ # Mock the Metadata class for rails 6
27
+ mock = OpenStruct.new(metadata: { width: width, height: height })
28
+ stub_method(ActiveStorageValidations::Metadata, :new, mock) do
29
+ yield
30
+ end
31
+ else
32
+ # Stub the metadata analysis for rails 5
33
+ stub_method(attachment, :analyze, true) do
34
+ stub_method(attachment, :analyzed?, true) do
35
+ stub_method(attachment, :metadata, { width: width, height: height }) do
36
+ yield
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorageValidations
4
+ module Matchers
5
+ def validate_attached_of(name)
6
+ AttachedValidatorMatcher.new(name)
7
+ end
8
+
9
+ class AttachedValidatorMatcher
10
+ def initialize(attribute_name)
11
+ @attribute_name = attribute_name
12
+ end
13
+
14
+ def description
15
+ "validate #{@attribute_name} must be attached"
16
+ end
17
+
18
+ def matches?(subject)
19
+ @subject = subject.is_a?(Class) ? subject : subject.class
20
+
21
+ invalid_when_not_attached && valid_when_attached
22
+ end
23
+
24
+ def failure_message
25
+ "is expected to validate attached of #{@attribute_name}"
26
+ end
27
+
28
+ def failure_message_when_negated
29
+ "is expected to not validate attached of #{@attribute_name}"
30
+ end
31
+
32
+ private
33
+
34
+ def valid_when_attached
35
+ instance = @subject.new
36
+ instance.public_send(@attribute_name).attach(attachable)
37
+ instance.validate
38
+ instance.errors.details[@attribute_name].exclude?(error: :blank)
39
+ end
40
+
41
+ def invalid_when_not_attached
42
+ instance = @subject.new
43
+ instance.validate
44
+ instance.errors.details[@attribute_name].include?(error: :blank)
45
+ end
46
+
47
+ def attachable
48
+ { io: Tempfile.new('.'), filename: 'dummy.txt', content_type: 'text/plain' }
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Big thank you to the paperclip validation matchers:
4
+ # https://github.com/thoughtbot/paperclip/blob/v6.1.0/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb
5
+ module ActiveStorageValidations
6
+ module Matchers
7
+ def validate_content_type_of(name)
8
+ ContentTypeValidatorMatcher.new(name)
9
+ end
10
+
11
+ class ContentTypeValidatorMatcher
12
+ def initialize(attribute_name)
13
+ @attribute_name = attribute_name
14
+ end
15
+
16
+ def description
17
+ "validate the content types allowed on attachment #{@attribute_name}"
18
+ end
19
+
20
+ def allowing(*types)
21
+ @allowed_types = types.flatten
22
+ self
23
+ end
24
+
25
+ def rejecting(*types)
26
+ @rejected_types = types.flatten
27
+ self
28
+ end
29
+
30
+ def matches?(subject)
31
+ @subject = subject.is_a?(Class) ? subject.new : subject
32
+ allowed_types_allowed? && rejected_types_rejected?
33
+ end
34
+
35
+ def failure_message
36
+ <<~MESSAGE
37
+ Expected #{@attribute_name}
38
+
39
+ Accept content types: #{allowed_types.join(", ")}
40
+ #{accepted_types_and_failures}
41
+
42
+ Reject content types: #{rejected_types.join(", ")}
43
+ #{rejected_types_and_failures}
44
+ MESSAGE
45
+ end
46
+
47
+ protected
48
+
49
+ def allowed_types
50
+ @allowed_types || []
51
+ end
52
+
53
+ def rejected_types
54
+ @rejected_types || (Mime::LOOKUP.keys - allowed_types)
55
+ end
56
+
57
+ def allowed_types_allowed?
58
+ @missing_allowed_types ||= allowed_types.reject { |type| type_allowed?(type) }
59
+ @missing_allowed_types.none?
60
+ end
61
+
62
+ def rejected_types_rejected?
63
+ @missing_rejected_types ||= rejected_types.select { |type| type_allowed?(type) }
64
+ @missing_rejected_types.none?
65
+ end
66
+
67
+ def accepted_types_and_failures
68
+ if @missing_allowed_types.present?
69
+ "#{@missing_allowed_types.join(", ")} were rejected."
70
+ else
71
+ "All were accepted successfully."
72
+ end
73
+ end
74
+
75
+ def rejected_types_and_failures
76
+ if @missing_rejected_types.present?
77
+ "#{@missing_rejected_types.join(", ")} were accepted."
78
+ else
79
+ "All were rejected successfully."
80
+ end
81
+ end
82
+
83
+ def type_allowed?(type)
84
+ @subject.public_send(@attribute_name).attach(attachment_for(type))
85
+ @subject.validate
86
+ @subject.errors.details[@attribute_name].all? { |error| error[:error] != :content_type_invalid }
87
+ end
88
+
89
+ def attachment_for(type)
90
+ suffix = type.to_s.split('/').last
91
+ { io: Tempfile.new('.'), filename: "test.#{suffix}", content_type: type }
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorageValidations
4
+ module Matchers
5
+ def validate_dimensions_of(name)
6
+ DimensionValidatorMatcher.new(name)
7
+ end
8
+
9
+ class DimensionValidatorMatcher
10
+ def initialize(attribute_name)
11
+ @attribute_name = attribute_name
12
+ @width_min = @width_max = @height_min = @height_max = nil
13
+ end
14
+
15
+ def description
16
+ "validate image dimensions of #{@attribute_name}"
17
+ end
18
+
19
+ def width_min(width)
20
+ @width_min = width
21
+ self
22
+ end
23
+
24
+ def width_max(width)
25
+ @width_max = width
26
+ self
27
+ end
28
+
29
+ def width(width)
30
+ @width_min = @width_max = width
31
+ self
32
+ end
33
+
34
+ def height_min(height)
35
+ @height_min = height
36
+ self
37
+ end
38
+
39
+ def height_max(height)
40
+ @height_max = height
41
+ self
42
+ end
43
+
44
+ def width_between(range)
45
+ @width_min, @width_max = range.first, range.last
46
+ self
47
+ end
48
+
49
+ def height_between(range)
50
+ @height_min, @height_max = range.first, range.last
51
+ self
52
+ end
53
+
54
+ def height(height)
55
+ @height_min = @height_max = height
56
+ self
57
+ end
58
+
59
+ def matches?(subject)
60
+ @subject = subject.is_a?(Class) ? subject.new : subject
61
+ width_smaller_than_min? && width_larger_than_min? && width_smaller_than_max? && width_larger_than_max? && width_equals? &&
62
+ height_smaller_than_min? && height_larger_than_min? && height_smaller_than_max? && height_larger_than_max? && height_equals?
63
+ end
64
+
65
+ def failure_message
66
+ <<~MESSAGE
67
+ is expected to validate dimensions of #{@attribute_name}
68
+ width between #{@width_min} and #{@width_max}
69
+ height between #{@height_min} and #{@height_max}
70
+ MESSAGE
71
+ end
72
+
73
+ protected
74
+
75
+ def valid_width
76
+ ((@width_min || 0) + (@width_max || 2000)) / 2
77
+ end
78
+
79
+ def valid_height
80
+ ((@height_min || 0) + (@height_max || 2000)) / 2
81
+ end
82
+
83
+ def width_smaller_than_min?
84
+ @width_min.nil? || !passes_validation_with_dimensions(@width_min - 1, valid_height, 'width')
85
+ end
86
+
87
+ def width_larger_than_min?
88
+ @width_min.nil? || @width_min == @width_max || passes_validation_with_dimensions(@width_min + 1, valid_height, 'width')
89
+ end
90
+
91
+ def width_smaller_than_max?
92
+ @width_max.nil? || @width_min == @width_max || passes_validation_with_dimensions(@width_max - 1, valid_height, 'width')
93
+ end
94
+
95
+ def width_larger_than_max?
96
+ @width_max.nil? || !passes_validation_with_dimensions(@width_max + 1, valid_height, 'width')
97
+ end
98
+
99
+ def width_equals?
100
+ @width_min.nil? || @width_min != @width_max || passes_validation_with_dimensions(@width_min, valid_height, 'width')
101
+ end
102
+
103
+ def height_smaller_than_min?
104
+ @height_min.nil? || !passes_validation_with_dimensions(valid_width, @height_min - 1, 'height')
105
+ end
106
+
107
+ def height_larger_than_min?
108
+ @height_min.nil? || @height_min == @height_max || passes_validation_with_dimensions(valid_width, @height_min + 1, 'height')
109
+ end
110
+
111
+ def height_smaller_than_max?
112
+ @height_max.nil? || @height_min == @height_max || passes_validation_with_dimensions(valid_width, @height_max - 1, 'height')
113
+ end
114
+
115
+ def height_larger_than_max?
116
+ @height_max.nil? || !passes_validation_with_dimensions(valid_width, @height_max + 1, 'height')
117
+ end
118
+
119
+ def height_equals?
120
+ @height_min.nil? || @height_min != @height_max || passes_validation_with_dimensions(valid_width, @height_min, 'height')
121
+ end
122
+
123
+ def passes_validation_with_dimensions(width, height, check)
124
+ @subject.public_send(@attribute_name).attach attachment_for(width, height)
125
+
126
+ attachment = @subject.public_send(@attribute_name)
127
+ Matchers.mock_metadata(attachment, width, height) do
128
+ @subject.validate
129
+ @subject.errors.details[@attribute_name].all? { |error| error[:error].to_s.exclude?("dimension_#{check}") }
130
+ end
131
+ end
132
+
133
+ def attachment_for(width, height)
134
+ { io: Tempfile.new('Hello world!'), filename: 'test.png', content_type: 'image/png' }
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Big thank you to the paperclip validation matchers:
4
+ # https://github.com/thoughtbot/paperclip/blob/v6.1.0/lib/paperclip/matchers/validate_attachment_size_matcher.rb
5
+ module ActiveStorageValidations
6
+ module Matchers
7
+ def validate_size_of(name)
8
+ SizeValidatorMatcher.new(name)
9
+ end
10
+
11
+ class SizeValidatorMatcher
12
+ def initialize(attribute_name)
13
+ @attribute_name = attribute_name
14
+ @low = @high = nil
15
+ end
16
+
17
+ def description
18
+ "validate file size of #{@attribute_name}"
19
+ end
20
+
21
+ def less_than(size)
22
+ @high = size - 1.byte
23
+ self
24
+ end
25
+
26
+ def less_than_or_equal_to(size)
27
+ @high = size
28
+ self
29
+ end
30
+
31
+ def greater_than(size)
32
+ @low = size + 1.byte
33
+ self
34
+ end
35
+
36
+ def greater_than_or_equal_to(size)
37
+ @low = size
38
+ self
39
+ end
40
+
41
+ def between(range)
42
+ @low, @high = range.first, range.last
43
+ self
44
+ end
45
+
46
+ def matches?(subject)
47
+ @subject = subject.is_a?(Class) ? subject.new : subject
48
+ lower_than_low? && higher_than_low? && lower_than_high? && higher_than_high?
49
+ end
50
+
51
+ def failure_message
52
+ "is expected to validate file size of #{@attribute_name} to be between #{@low} and #{@high} bytes"
53
+ end
54
+
55
+ def failure_message_when_negated
56
+ "is expected to not validate file size of #{@attribute_name} to be between #{@low} and #{@high} bytes"
57
+ end
58
+
59
+ protected
60
+
61
+ def lower_than_low?
62
+ @low.nil? || !passes_validation_with_size(@low - 1)
63
+ end
64
+
65
+ def higher_than_low?
66
+ @low.nil? || passes_validation_with_size(@low + 1)
67
+ end
68
+
69
+ def lower_than_high?
70
+ @high.nil? || @high == Float::INFINITY || passes_validation_with_size(@high - 1)
71
+ end
72
+
73
+ def higher_than_high?
74
+ @high.nil? || @high == Float::INFINITY || !passes_validation_with_size(@high + 1)
75
+ end
76
+
77
+ def passes_validation_with_size(new_size)
78
+ io = Tempfile.new('Hello world!')
79
+ Matchers.stub_method(io, :size, new_size) do
80
+ @subject.public_send(@attribute_name).attach(io: io, filename: 'test.png', content_type: 'image/pg')
81
+ @subject.validate
82
+ @subject.errors.details[@attribute_name].all? { |error| error[:error] != :file_size_out_of_range }
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorageValidations
4
- VERSION = '0.8.6'
4
+ VERSION = '0.8.7'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_storage_validations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.6
4
+ version: 0.8.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Kasyanchuk
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-01-20 00:00:00.000000000 Z
11
+ date: 2020-01-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -117,6 +117,11 @@ files:
117
117
  - lib/active_storage_validations/dimension_validator.rb
118
118
  - lib/active_storage_validations/engine.rb
119
119
  - lib/active_storage_validations/limit_validator.rb
120
+ - lib/active_storage_validations/matchers.rb
121
+ - lib/active_storage_validations/matchers/attached_validator_matcher.rb
122
+ - lib/active_storage_validations/matchers/content_type_validator_matcher.rb
123
+ - lib/active_storage_validations/matchers/dimension_validator_matcher.rb
124
+ - lib/active_storage_validations/matchers/size_validator_matcher.rb
120
125
  - lib/active_storage_validations/metadata.rb
121
126
  - lib/active_storage_validations/railtie.rb
122
127
  - lib/active_storage_validations/size_validator.rb