active_storage_validations 0.8.6 → 0.8.7

Sign up to get free protection for your applications and to get access to all the features.
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