shoulda-matchers 5.3.0 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +22 -7
  4. data/lib/shoulda/matchers/action_controller/permit_matcher.rb +1 -1
  5. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +9 -0
  6. data/lib/shoulda/matchers/active_model/comparison_matcher.rb +162 -0
  7. data/lib/shoulda/matchers/active_model/numericality_matchers/range_matcher.rb +1 -1
  8. data/lib/shoulda/matchers/active_model/numericality_matchers/submatchers.rb +21 -6
  9. data/lib/shoulda/matchers/active_model/validate_comparison_of_matcher.rb +534 -0
  10. data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +3 -3
  11. data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +4 -3
  12. data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +64 -9
  13. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +32 -86
  14. data/lib/shoulda/matchers/active_model/validation_matcher.rb +6 -0
  15. data/lib/shoulda/matchers/active_model/validator.rb +4 -0
  16. data/lib/shoulda/matchers/active_model.rb +2 -1
  17. data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +0 -8
  18. data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +46 -6
  19. data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +14 -3
  20. data/lib/shoulda/matchers/active_record/have_implicit_order_column.rb +3 -5
  21. data/lib/shoulda/matchers/active_record/normalize_matcher.rb +151 -0
  22. data/lib/shoulda/matchers/active_record.rb +1 -0
  23. data/lib/shoulda/matchers/independent/delegate_method_matcher.rb +1 -1
  24. data/lib/shoulda/matchers/rails_shim.rb +8 -6
  25. data/lib/shoulda/matchers/util/word_wrap.rb +1 -1
  26. data/lib/shoulda/matchers/util.rb +1 -1
  27. data/lib/shoulda/matchers/version.rb +1 -1
  28. data/lib/shoulda/matchers.rb +2 -2
  29. data/shoulda-matchers.gemspec +1 -1
  30. metadata +10 -8
  31. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +0 -136
@@ -0,0 +1,151 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module ActiveRecord
4
+ # The `normalize` matcher is used to ensure attribute normalizations
5
+ # are transforming attribute values as expected.
6
+ #
7
+ # Take this model for example:
8
+ #
9
+ # class User < ActiveRecord::Base
10
+ # normalizes :email, with: -> email { email.strip.downcase }
11
+ # end
12
+ #
13
+ # You can use `normalize` providing an input and defining the expected
14
+ # normalization output:
15
+ #
16
+ # # RSpec
17
+ # RSpec.describe User, type: :model do
18
+ # it do
19
+ # should normalize(:email).from(" ME@XYZ.COM\n").to("me@xyz.com")
20
+ # end
21
+ # end
22
+ #
23
+ # # Minitest (Shoulda)
24
+ # class User < ActiveSupport::TestCase
25
+ # should normalize(:email).from(" ME@XYZ.COM\n").to("me@xyz.com")
26
+ # end
27
+ #
28
+ # You can use `normalize` to test multiple attributes at once:
29
+ #
30
+ # class User < ActiveRecord::Base
31
+ # normalizes :email, :handle, with: -> value { value.strip.downcase }
32
+ # end
33
+ #
34
+ # # RSpec
35
+ # RSpec.describe User, type: :model do
36
+ # it do
37
+ # should normalize(:email, :handle).from(" Example\n").to("example")
38
+ # end
39
+ # end
40
+ #
41
+ # # Minitest (Shoulda)
42
+ # class User < ActiveSupport::TestCase
43
+ # should normalize(:email, handle).from(" Example\n").to("example")
44
+ # end
45
+ #
46
+ # If the normalization accepts nil values with the `apply_to_nil` option,
47
+ # you just need to use `.from(nil).to("Your expected value here")`.
48
+ #
49
+ # class User < ActiveRecord::Base
50
+ # normalizes :name, with: -> name { name&.titleize || 'Untitled' },
51
+ # apply_to_nil: true
52
+ # end
53
+ #
54
+ # # RSpec
55
+ # RSpec.describe User, type: :model do
56
+ # it { should normalize(:name).from("jane doe").to("Jane Doe") }
57
+ # it { should normalize(:name).from(nil).to("Untitled") }
58
+ # end
59
+ #
60
+ # # Minitest (Shoulda)
61
+ # class User < ActiveSupport::TestCase
62
+ # should normalize(:name).from("jane doe").to("Jane Doe")
63
+ # should normalize(:name).from(nil).to("Untitled")
64
+ # end
65
+ #
66
+ # @return [NormalizeMatcher]
67
+ #
68
+ def normalize(*attributes)
69
+ if attributes.empty?
70
+ raise ArgumentError, 'need at least one attribute'
71
+ else
72
+ NormalizeMatcher.new(*attributes)
73
+ end
74
+ end
75
+
76
+ # @private
77
+ class NormalizeMatcher
78
+ attr_reader :attributes, :from_value, :to_value, :failure_message,
79
+ :failure_message_when_negated
80
+
81
+ def initialize(*attributes)
82
+ @attributes = attributes
83
+ end
84
+
85
+ def description
86
+ %(
87
+ normalize #{attributes.to_sentence(last_word_connector: ' and ')} from
88
+ ‹#{from_value.inspect}› to ‹#{to_value.inspect}›
89
+ ).squish
90
+ end
91
+
92
+ def from(value)
93
+ @from_value = value
94
+
95
+ self
96
+ end
97
+
98
+ def to(value)
99
+ @to_value = value
100
+
101
+ self
102
+ end
103
+
104
+ def matches?(subject)
105
+ attributes.all? { |attribute| attribute_matches?(subject, attribute) }
106
+ end
107
+
108
+ def does_not_match?(subject)
109
+ attributes.all? { |attribute| attribute_does_not_match?(subject, attribute) }
110
+ end
111
+
112
+ private
113
+
114
+ def attribute_matches?(subject, attribute)
115
+ return true if normalize_attribute?(subject, attribute)
116
+
117
+ @failure_message = build_failure_message(
118
+ attribute,
119
+ subject.class.normalize_value_for(attribute, from_value),
120
+ )
121
+ false
122
+ end
123
+
124
+ def attribute_does_not_match?(subject, attribute)
125
+ return true unless normalize_attribute?(subject, attribute)
126
+
127
+ @failure_message_when_negated = build_failure_message_when_negated(attribute)
128
+ false
129
+ end
130
+
131
+ def normalize_attribute?(subject, attribute)
132
+ subject.class.normalize_value_for(attribute, from_value) == to_value
133
+ end
134
+
135
+ def build_failure_message(attribute, attribute_value)
136
+ %(
137
+ Expected to normalize #{attribute.inspect} from ‹#{from_value.inspect}› to
138
+ ‹#{to_value.inspect}› but it was normalized to ‹#{attribute_value.inspect}›
139
+ ).squish
140
+ end
141
+
142
+ def build_failure_message_when_negated(attribute)
143
+ %(
144
+ Expected to not normalize #{attribute.inspect} from ‹#{from_value.inspect}› to
145
+ ‹#{to_value.inspect}› but it was normalized
146
+ ).squish
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -24,6 +24,7 @@ require 'shoulda/matchers/active_record/define_enum_for_matcher'
24
24
  require 'shoulda/matchers/active_record/uniqueness'
25
25
  require 'shoulda/matchers/active_record/validate_uniqueness_of_matcher'
26
26
  require 'shoulda/matchers/active_record/have_attached_matcher'
27
+ require 'shoulda/matchers/active_record/normalize_matcher'
27
28
 
28
29
  module Shoulda
29
30
  module Matchers
@@ -400,7 +400,7 @@ module Shoulda
400
400
  false
401
401
  rescue NoMethodError => e
402
402
  if e.message =~
403
- /undefined method `#{delegate_method}' for nil:NilClass/
403
+ /undefined method `#{delegate_method}' for nil/
404
404
  false
405
405
  else
406
406
  raise e
@@ -1,7 +1,7 @@
1
1
  module Shoulda
2
2
  module Matchers
3
3
  # @private
4
- module RailsShim # rubocop:disable Metrics/ModuleLength
4
+ module RailsShim
5
5
  class << self
6
6
  def action_pack_version
7
7
  Gem::Version.new(::ActionPack::VERSION::STRING)
@@ -9,10 +9,6 @@ module Shoulda
9
9
  Gem::Version.new('0')
10
10
  end
11
11
 
12
- def active_record_gte_6?
13
- Gem::Requirement.new('>= 6').satisfied_by?(active_record_version)
14
- end
15
-
16
12
  def active_record_version
17
13
  Gem::Version.new(::ActiveRecord::VERSION::STRING)
18
14
  rescue NameError
@@ -60,9 +56,10 @@ module Shoulda
60
56
  end
61
57
 
62
58
  def serialized_attributes_for(model)
59
+ type_serialized_defined = Object.const_defined?('ActiveRecord::Type::Serialized')
63
60
  attribute_types_for(model).
64
61
  inject({}) do |hash, (attribute_name, attribute_type)|
65
- if attribute_type.is_a?(::ActiveRecord::Type::Serialized)
62
+ if type_serialized_defined && attribute_type.is_a?(::ActiveRecord::Type::Serialized)
66
63
  hash.merge(attribute_name => attribute_type.coder)
67
64
  else
68
65
  hash
@@ -146,6 +143,10 @@ module Shoulda
146
143
  model.respond_to?(:attribute_types)
147
144
  end
148
145
 
146
+ def validates_column_options?
147
+ Gem::Requirement.new('>= 7.1.0').satisfied_by?(active_record_version)
148
+ end
149
+
149
150
  private
150
151
 
151
152
  def simply_generate_validation_message(
@@ -172,6 +173,7 @@ module Shoulda
172
173
  I18n.translate(primary_translation_key, translate_options)
173
174
  end
174
175
 
176
+ # @private
175
177
  class FakeAttributeType
176
178
  def initialize(model, attribute_name)
177
179
  @model = model
@@ -41,7 +41,7 @@ module Shoulda
41
41
 
42
42
  # @private
43
43
  class Text < ::String
44
- LIST_ITEM_REGEXP = /\A((?:[a-z0-9]+(?:\)|\.)|\*) )/.freeze
44
+ LIST_ITEM_REGEXP = /\A((?:[a-z0-9]+(?:\)|\.)|\*) )/
45
45
 
46
46
  def indented?
47
47
  self =~ /\A +/
@@ -94,7 +94,7 @@ module Shoulda
94
94
  0
95
95
  when :date
96
96
  Date.new(2100, 1, 1)
97
- when :datetime, :timestamp
97
+ when :datetime, :timestamp, :timestamptz
98
98
  DateTime.new(2100, 1, 1)
99
99
  when :time
100
100
  Time.new(2000, 1, 1)
@@ -1,6 +1,6 @@
1
1
  module Shoulda
2
2
  module Matchers
3
3
  # @private
4
- VERSION = '5.3.0'.freeze
4
+ VERSION = '6.0.0'.freeze
5
5
  end
6
6
  end
@@ -14,8 +14,8 @@ require 'shoulda/matchers/active_model'
14
14
  require 'shoulda/matchers/active_record'
15
15
  require 'shoulda/matchers/routing'
16
16
 
17
- module Shoulda
18
- module Matchers
17
+ module Shoulda # :nodoc:
18
+ module Matchers # :nodoc:
19
19
  class << self
20
20
  # @private
21
21
  attr_accessor :assertion_exception_class
@@ -36,6 +36,6 @@ Gem::Specification.new do |s|
36
36
  'shoulda-matchers.gemspec']
37
37
  s.require_paths = ['lib']
38
38
 
39
- s.required_ruby_version = '>= 2.6.0'
39
+ s.required_ruby_version = '>= 3.0.5'
40
40
  s.add_dependency('activesupport', '>= 5.2.0')
41
41
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shoulda-matchers
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.3.0
4
+ version: 6.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tammer Saleh
@@ -11,10 +11,10 @@ authors:
11
11
  - Matt Jankowski
12
12
  - Stafford Brunk
13
13
  - Elliot Winkler
14
- autorequire:
14
+ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
- date: 2022-12-16 00:00:00.000000000 Z
17
+ date: 2023-12-22 00:00:00.000000000 Z
18
18
  dependencies:
19
19
  - !ruby/object:Gem::Dependency
20
20
  name: activesupport
@@ -69,12 +69,12 @@ files:
69
69
  - lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters_and_validators.rb
70
70
  - lib/shoulda/matchers/active_model/allow_value_matcher/successful_check.rb
71
71
  - lib/shoulda/matchers/active_model/allow_value_matcher/successful_setting.rb
72
+ - lib/shoulda/matchers/active_model/comparison_matcher.rb
72
73
  - lib/shoulda/matchers/active_model/disallow_value_matcher.rb
73
74
  - lib/shoulda/matchers/active_model/errors.rb
74
75
  - lib/shoulda/matchers/active_model/have_secure_password_matcher.rb
75
76
  - lib/shoulda/matchers/active_model/helpers.rb
76
77
  - lib/shoulda/matchers/active_model/numericality_matchers.rb
77
- - lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb
78
78
  - lib/shoulda/matchers/active_model/numericality_matchers/even_number_matcher.rb
79
79
  - lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb
80
80
  - lib/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher.rb
@@ -88,6 +88,7 @@ files:
88
88
  - lib/shoulda/matchers/active_model/qualifiers/ignoring_interference_by_writer.rb
89
89
  - lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb
90
90
  - lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb
91
+ - lib/shoulda/matchers/active_model/validate_comparison_of_matcher.rb
91
92
  - lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb
92
93
  - lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb
93
94
  - lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb
@@ -122,6 +123,7 @@ files:
122
123
  - lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb
123
124
  - lib/shoulda/matchers/active_record/have_rich_text_matcher.rb
124
125
  - lib/shoulda/matchers/active_record/have_secure_token_matcher.rb
126
+ - lib/shoulda/matchers/active_record/normalize_matcher.rb
125
127
  - lib/shoulda/matchers/active_record/serialize_matcher.rb
126
128
  - lib/shoulda/matchers/active_record/uniqueness.rb
127
129
  - lib/shoulda/matchers/active_record/uniqueness/model.rb
@@ -180,7 +182,7 @@ metadata:
180
182
  documentation_uri: https://matchers.shoulda.io/docs
181
183
  homepage_uri: https://matchers.shoulda.io
182
184
  source_code_uri: https://github.com/thoughtbot/shoulda-matchers
183
- post_install_message:
185
+ post_install_message:
184
186
  rdoc_options: []
185
187
  require_paths:
186
188
  - lib
@@ -188,15 +190,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
188
190
  requirements:
189
191
  - - ">="
190
192
  - !ruby/object:Gem::Version
191
- version: 2.6.0
193
+ version: 3.0.5
192
194
  required_rubygems_version: !ruby/object:Gem::Requirement
193
195
  requirements:
194
196
  - - ">="
195
197
  - !ruby/object:Gem::Version
196
198
  version: '0'
197
199
  requirements: []
198
- rubygems_version: 3.3.7
199
- signing_key:
200
+ rubygems_version: 3.4.20
201
+ signing_key:
200
202
  specification_version: 4
201
203
  summary: Simple one-liner tests for common Rails functionality
202
204
  test_files: []
@@ -1,136 +0,0 @@
1
- require 'active_support/core_ext/module/delegation'
2
-
3
- module Shoulda
4
- module Matchers
5
- module ActiveModel
6
- module NumericalityMatchers
7
- # @private
8
- class ComparisonMatcher < ValidationMatcher
9
- ERROR_MESSAGES = {
10
- :> => {
11
- label: :greater_than,
12
- assertions: [false, false, true],
13
- },
14
- :>= => {
15
- label: :greater_than_or_equal_to,
16
- assertions: [false, true, true],
17
- },
18
- :< => {
19
- label: :less_than,
20
- assertions: [true, false, false],
21
- },
22
- :<= => {
23
- label: :less_than_or_equal_to,
24
- assertions: [true, true, false],
25
- },
26
- :== => {
27
- label: :equal_to,
28
- assertions: [false, true, false],
29
- },
30
- :!= => {
31
- label: :other_than,
32
- assertions: [true, false, true],
33
- },
34
- }.freeze
35
-
36
- delegate :failure_message, :failure_message_when_negated, to: :submatchers
37
-
38
- def initialize(numericality_matcher, value, operator)
39
- super(nil)
40
- unless numericality_matcher.respond_to? :diff_to_compare
41
- raise ArgumentError, 'numericality_matcher is invalid'
42
- end
43
-
44
- @numericality_matcher = numericality_matcher
45
- @value = value
46
- @operator = operator
47
- @message = ERROR_MESSAGES[operator][:label]
48
- end
49
-
50
- def simple_description
51
- description = ''
52
-
53
- if expects_strict?
54
- description << ' strictly'
55
- end
56
-
57
- description +
58
- "disallow :#{attribute} from being a number that is not " +
59
- "#{comparison_expectation} #{@value}"
60
- end
61
-
62
- def for(attribute)
63
- @attribute = attribute
64
- self
65
- end
66
-
67
- def with_message(message)
68
- @expects_custom_validation_message = true
69
- @message = message
70
- self
71
- end
72
-
73
- def expects_custom_validation_message?
74
- @expects_custom_validation_message
75
- end
76
-
77
- def matches?(subject)
78
- @subject = subject
79
- submatchers.matches?(subject)
80
- end
81
-
82
- def comparison_description
83
- "#{comparison_expectation} #{@value}"
84
- end
85
-
86
- def submatchers
87
- @_submatchers ||= NumericalityMatchers::Submatchers.new(build_submatchers)
88
- end
89
-
90
- private
91
-
92
- def build_submatchers
93
- comparison_combos.map do |diff, submatcher_method_name|
94
- matcher = __send__(submatcher_method_name, diff, nil)
95
- matcher.with_message(@message, values: { count: @value })
96
- matcher
97
- end
98
- end
99
-
100
- def comparison_combos
101
- diffs_to_compare.zip(submatcher_method_names)
102
- end
103
-
104
- def submatcher_method_names
105
- assertions.map do |value|
106
- if value
107
- :allow_value_matcher
108
- else
109
- :disallow_value_matcher
110
- end
111
- end
112
- end
113
-
114
- def assertions
115
- ERROR_MESSAGES[@operator][:assertions]
116
- end
117
-
118
- def diffs_to_compare
119
- diff_to_compare = @numericality_matcher.diff_to_compare
120
- values = [-1, 0, 1].map { |sign| @value + (diff_to_compare * sign) }
121
-
122
- if @numericality_matcher.given_numeric_column?
123
- values
124
- else
125
- values.map(&:to_s)
126
- end
127
- end
128
-
129
- def comparison_expectation
130
- ERROR_MESSAGES[@operator][:label].to_s.tr('_', ' ')
131
- end
132
- end
133
- end
134
- end
135
- end
136
- end