shoulda-matchers 4.1.1 → 4.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +163 -79
  3. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +2 -2
  4. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +5 -1
  5. data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +1 -1
  6. data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +2 -21
  7. data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +27 -3
  8. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +32 -0
  9. data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +6 -9
  10. data/lib/shoulda/matchers/active_model/validation_matcher.rb +25 -0
  11. data/lib/shoulda/matchers/active_record.rb +3 -0
  12. data/lib/shoulda/matchers/active_record/association_matcher.rb +23 -7
  13. data/lib/shoulda/matchers/active_record/association_matchers/join_table_matcher.rb +2 -2
  14. data/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +12 -6
  15. data/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +20 -3
  16. data/lib/shoulda/matchers/active_record/have_attached_matcher.rb +147 -0
  17. data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +1 -1
  18. data/lib/shoulda/matchers/active_record/have_implicit_order_column.rb +106 -0
  19. data/lib/shoulda/matchers/active_record/have_rich_text_matcher.rb +79 -0
  20. data/lib/shoulda/matchers/active_record/have_secure_token_matcher.rb +28 -9
  21. data/lib/shoulda/matchers/active_record/serialize_matcher.rb +2 -6
  22. data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +2 -2
  23. data/lib/shoulda/matchers/independent.rb +0 -1
  24. data/lib/shoulda/matchers/rails_shim.rb +42 -44
  25. data/lib/shoulda/matchers/util.rb +9 -2
  26. data/lib/shoulda/matchers/util/word_wrap.rb +1 -1
  27. data/lib/shoulda/matchers/version.rb +1 -1
  28. metadata +9 -8
  29. data/MIT-LICENSE +0 -22
  30. data/lib/shoulda/matchers/independent/delegate_method_matcher/stubbed_target.rb +0 -37
@@ -269,7 +269,7 @@ module Shoulda
269
269
  end
270
270
 
271
271
  def type_cast_default
272
- Shoulda::Matchers::RailsShim.type_cast_default_for(model, self)
272
+ model.column_defaults[name]
273
273
  end
274
274
 
275
275
  def primary?
@@ -0,0 +1,106 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module ActiveRecord
4
+ # The `have_implicit_order_column` matcher tests that the model has `implicit_order_column`
5
+ # assigned to one of the table columns. (Rails 6+ only)
6
+ #
7
+ # class Product < ApplicationRecord
8
+ # self.implicit_order_column = :created_at
9
+ # end
10
+ #
11
+ # # RSpec
12
+ # RSpec.describe Product, type: :model do
13
+ # it { should have_implicit_order_column(:created_at) }
14
+ # end
15
+ #
16
+ # # Minitest (Shoulda)
17
+ # class ProductTest < ActiveSupport::TestCase
18
+ # should have_implicit_order_column(:created_at)
19
+ # end
20
+ #
21
+ # @return [HaveImplicitOrderColumnMatcher]
22
+ #
23
+ if RailsShim.active_record_gte_6?
24
+ def have_implicit_order_column(column_name)
25
+ HaveImplicitOrderColumnMatcher.new(column_name)
26
+ end
27
+ end
28
+
29
+ # @private
30
+ class HaveImplicitOrderColumnMatcher
31
+ attr_reader :failure_message
32
+
33
+ def initialize(column_name)
34
+ @column_name = column_name
35
+ end
36
+
37
+ def matches?(subject)
38
+ @subject = subject
39
+ check_column_exists!
40
+ check_implicit_order_column_matches!
41
+ true
42
+ rescue SecondaryCheckFailedError => error
43
+ @failure_message = Shoulda::Matchers.word_wrap(
44
+ "Expected #{model.name} to #{expectation}, " +
45
+ "but that could not be proved: #{error.message}."
46
+ )
47
+ false
48
+ rescue PrimaryCheckFailedError => error
49
+ @failure_message = Shoulda::Matchers.word_wrap(
50
+ "Expected #{model.name} to #{expectation}, but #{error.message}."
51
+ )
52
+ false
53
+ end
54
+
55
+ def failure_message_when_negated
56
+ Shoulda::Matchers.word_wrap(
57
+ "Expected #{model.name} not to #{expectation}, but it did."
58
+ )
59
+ end
60
+
61
+ def description
62
+ expectation
63
+ end
64
+
65
+ private
66
+
67
+ attr_reader :column_name, :subject
68
+
69
+ def check_column_exists!
70
+ matcher = HaveDbColumnMatcher.new(column_name)
71
+
72
+ if !matcher.matches?(@subject)
73
+ raise SecondaryCheckFailedError.new(
74
+ "The :#{model.table_name} table does not have a " +
75
+ ":#{column_name} column"
76
+ )
77
+ end
78
+ end
79
+
80
+ def check_implicit_order_column_matches!
81
+ if model.implicit_order_column.to_s != column_name.to_s
82
+ message =
83
+ if model.implicit_order_column.nil?
84
+ "implicit_order_column is not set"
85
+ else
86
+ "it is :#{model.implicit_order_column}"
87
+ end
88
+
89
+ raise PrimaryCheckFailedError.new(message)
90
+ end
91
+ end
92
+
93
+ def model
94
+ subject.class
95
+ end
96
+
97
+ def expectation
98
+ "have an implicit_order_column of :#{column_name}"
99
+ end
100
+
101
+ class SecondaryCheckFailedError < StandardError; end
102
+ class PrimaryCheckFailedError < StandardError; end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,79 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module ActiveRecord
4
+ # The `have_rich_text` matcher tests usage of the
5
+ # `has_rich_text` macro.
6
+ #
7
+ # #### Example
8
+ #
9
+ # class Post < ActiveRecord
10
+ # has_rich_text :content
11
+ # end
12
+ #
13
+ # # RSpec
14
+ # RSpec.describe Post, type: :model do
15
+ # it { is_expected.to have_rich_text(:content) }
16
+ # end
17
+ #
18
+ # # Minitest (Shoulda)
19
+ # class PostTest < ActiveSupport::TestCase
20
+ # should have_rich_text(:content)
21
+ # end
22
+ #
23
+ # @return [HaveRichText]
24
+ #
25
+ def have_rich_text(rich_text_attribute)
26
+ HaveRichText.new(rich_text_attribute)
27
+ end
28
+
29
+ # @private
30
+ class HaveRichText
31
+ def initialize(rich_text_attribute)
32
+ @rich_text_attribute = rich_text_attribute
33
+ end
34
+
35
+ def description
36
+ "have configured :#{rich_text_attribute} as a ActionText::RichText association"
37
+ end
38
+
39
+ def failure_message
40
+ "Expected #{subject.class} to #{error_description}"
41
+ end
42
+
43
+ def failure_message_when_negated
44
+ "Did not expect #{subject.class} to have ActionText::RichText :#{rich_text_attribute}"
45
+ end
46
+
47
+ def matches?(subject)
48
+ @subject = subject
49
+ @error = run_checks
50
+ @error.nil?
51
+ end
52
+
53
+ private
54
+
55
+ attr_reader :error, :rich_text_attribute, :subject
56
+
57
+ def run_checks
58
+ if !has_attribute?
59
+ ":#{rich_text_attribute} does not exist"
60
+ elsif !has_expected_action_text?
61
+ :default
62
+ end
63
+ end
64
+
65
+ def has_attribute?
66
+ @subject.respond_to?(rich_text_attribute.to_s)
67
+ end
68
+
69
+ def has_expected_action_text?
70
+ @subject.send(rich_text_attribute).class.name == 'ActionText::RichText'
71
+ end
72
+
73
+ def error_description
74
+ error == :default ? description : "#{description} but #{error}"
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -4,12 +4,7 @@ module Shoulda
4
4
  # The `have_secure_token` matcher tests usage of the
5
5
  # `has_secure_token` macro.
6
6
  #
7
- # #### Example
8
- #
9
7
  # class User < ActiveRecord
10
- # attr_accessor :token
11
- # attr_accessor :auth_token
12
- #
13
8
  # has_secure_token
14
9
  # has_secure_token :auth_token
15
10
  # end
@@ -26,14 +21,32 @@ module Shoulda
26
21
  # should have_secure_token(:auth_token)
27
22
  # end
28
23
  #
24
+ # #### Qualifiers
25
+ #
26
+ # ##### ignoring_check_for_db_index
27
+ #
28
+ # By default, this matcher tests that an index is defined on your token
29
+ # column. Use `ignoring_check_for_db_index` if this is not the case.
30
+ #
31
+ # class User < ActiveRecord
32
+ # has_secure_token :auth_token
33
+ # end
34
+ #
35
+ # # RSpec
36
+ # RSpec.describe User, type: :model do
37
+ # it { should have_secure_token(:auth_token).ignoring_check_for_db_index }
38
+ # end
39
+ #
40
+ # # Minitest (Shoulda)
41
+ # class UserTest < ActiveSupport::TestCase
42
+ # should have_secure_token(:auth_token).ignoring_check_for_db_index
43
+ # end
44
+ #
29
45
  # @return [HaveSecureToken]
30
46
  #
31
-
32
- # rubocop:disable Style/PredicateName
33
47
  def have_secure_token(token_attribute = :token)
34
48
  HaveSecureTokenMatcher.new(token_attribute)
35
49
  end
36
- # rubocop:enable Style/PredicateName
37
50
 
38
51
  # @private
39
52
  class HaveSecureTokenMatcher
@@ -41,6 +54,7 @@ module Shoulda
41
54
 
42
55
  def initialize(token_attribute)
43
56
  @token_attribute = token_attribute
57
+ @options = { ignore_check_for_db_index: false }
44
58
  end
45
59
 
46
60
  def description
@@ -65,6 +79,11 @@ module Shoulda
65
79
  @errors.empty?
66
80
  end
67
81
 
82
+ def ignoring_check_for_db_index
83
+ @options[:ignore_check_for_db_index] = true
84
+ self
85
+ end
86
+
68
87
  private
69
88
 
70
89
  def run_checks
@@ -75,7 +94,7 @@ module Shoulda
75
94
  if !has_expected_db_column?
76
95
  @errors << "missing correct column #{token_attribute}:string"
77
96
  end
78
- if !has_expected_db_index?
97
+ if !@options[:ignore_check_for_db_index] && !has_expected_db_index?
79
98
  @errors << "missing unique index for #{table_and_column}"
80
99
  end
81
100
  @errors
@@ -183,15 +183,11 @@ module Shoulda
183
183
  end
184
184
 
185
185
  def attribute_is_serialized?
186
- serialized_attributes.include?(@name)
186
+ !!serialization_coder
187
187
  end
188
188
 
189
189
  def serialization_coder
190
- serialized_attributes[@name]
191
- end
192
-
193
- def serialized_attributes
194
- Shoulda::Matchers::RailsShim.serialized_attributes_for(model)
190
+ RailsShim.attribute_serialization_coder_for(model, @name)
195
191
  end
196
192
 
197
193
  def model
@@ -396,7 +396,7 @@ module Shoulda
396
396
  end
397
397
 
398
398
  def validations
399
- model._validators[@attribute].select do |validator|
399
+ model.validators_on(@attribute).select do |validator|
400
400
  validator.is_a?(::ActiveRecord::Validations::UniquenessValidator)
401
401
  end
402
402
  end
@@ -820,7 +820,7 @@ module Shoulda
820
820
  elsif previous_value.respond_to?(:next)
821
821
  previous_value.next
822
822
  elsif previous_value.respond_to?(:to_datetime)
823
- previous_value.to_datetime.next
823
+ previous_value.to_datetime.in(60).next
824
824
  elsif boolean_value?(previous_value)
825
825
  !previous_value
826
826
  else
@@ -1,5 +1,4 @@
1
1
  require 'shoulda/matchers/independent/delegate_method_matcher'
2
- require 'shoulda/matchers/independent/delegate_method_matcher/stubbed_target'
3
2
  require 'shoulda/matchers/independent/delegate_method_matcher/target_not_defined_error'
4
3
 
5
4
  module Shoulda
@@ -3,10 +3,6 @@ module Shoulda
3
3
  # @private
4
4
  module RailsShim
5
5
  class << self
6
- def action_pack_gte_4_1?
7
- Gem::Requirement.new('>= 4.1').satisfied_by?(action_pack_version)
8
- end
9
-
10
6
  def action_pack_gte_5?
11
7
  Gem::Requirement.new('>= 5').satisfied_by?(action_pack_version)
12
8
  end
@@ -25,6 +21,10 @@ module Shoulda
25
21
  Gem::Requirement.new('>= 5').satisfied_by?(active_record_version)
26
22
  end
27
23
 
24
+ def active_record_gte_6?
25
+ Gem::Requirement.new('>= 6').satisfied_by?(active_record_version)
26
+ end
27
+
28
28
  def active_record_version
29
29
  Gem::Version.new(::ActiveRecord::VERSION::STRING)
30
30
  rescue NameError
@@ -69,30 +69,20 @@ module Shoulda
69
69
  end
70
70
 
71
71
  def serialized_attributes_for(model)
72
- if defined?(::ActiveRecord::Type::Serialized)
73
- # Rails 5+
74
- serialized_columns = model.columns.select do |column|
75
- model.type_for_attribute(column.name).is_a?(
76
- ::ActiveRecord::Type::Serialized,
77
- )
78
- end
79
-
80
- serialized_columns.inject({}) do |hash, column|
81
- hash[column.name.to_s] = model.type_for_attribute(column.name).coder
82
- hash
72
+ attribute_types_for(model).
73
+ inject({}) do |hash, (attribute_name, attribute_type)|
74
+ if attribute_type.is_a?(::ActiveRecord::Type::Serialized)
75
+ hash.merge(attribute_name => attribute_type.coder)
76
+ else
77
+ hash
78
+ end
83
79
  end
84
- else
85
- model.serialized_attributes
86
- end
80
+ rescue NotImplementedError
81
+ {}
87
82
  end
88
83
 
89
- def type_cast_default_for(model, column)
90
- if model.respond_to?(:column_defaults)
91
- # Rails 4.2
92
- model.column_defaults[column.name]
93
- else
94
- column.default
95
- end
84
+ def attribute_serialization_coder_for(model, attribute_name)
85
+ serialized_attributes_for(model)[attribute_name.to_s]
96
86
  end
97
87
 
98
88
  def tables_and_views(connection)
@@ -104,11 +94,7 @@ module Shoulda
104
94
  end
105
95
 
106
96
  def verb_for_update
107
- if action_pack_gte_4_1?
108
- :patch
109
- else
110
- :put
111
- end
97
+ :patch
112
98
  end
113
99
 
114
100
  def validation_message_key_for_association_required_option
@@ -156,14 +142,35 @@ module Shoulda
156
142
  nil
157
143
  end
158
144
 
145
+ def attribute_types_for(model)
146
+ if model.respond_to?(:attribute_types)
147
+ model.attribute_types
148
+ elsif model.respond_to?(:type_for_attribute)
149
+ model.columns.inject({}) do |hash, column|
150
+ key = column.name.to_s
151
+ value = model.type_for_attribute(column.name)
152
+ hash.merge(key => value)
153
+ end
154
+ else
155
+ raise NotImplementedError
156
+ end
157
+ end
158
+
159
159
  def attribute_type_for(model, attribute_name)
160
- if supports_full_attributes_api?(model)
161
- model.attribute_types[attribute_name.to_s]
160
+ attribute_types_for(model)[attribute_name.to_s]
161
+ rescue NotImplementedError
162
+ if model.respond_to?(:type_for_attribute)
163
+ model.type_for_attribute(attribute_name)
162
164
  else
163
- LegacyAttributeType.new(model, attribute_name)
165
+ FakeAttributeType.new(model, attribute_name)
164
166
  end
165
167
  end
166
168
 
169
+ def supports_full_attributes_api?(model)
170
+ defined?(::ActiveModel::Attributes) &&
171
+ model.respond_to?(:attribute_types)
172
+ end
173
+
167
174
  private
168
175
 
169
176
  def simply_generate_validation_message(
@@ -188,23 +195,14 @@ module Shoulda
188
195
  I18n.translate(primary_translation_key, translate_options)
189
196
  end
190
197
 
191
- def supports_full_attributes_api?(model)
192
- defined?(::ActiveModel::Attributes) &&
193
- model.respond_to?(:attribute_types)
194
- end
195
-
196
- class LegacyAttributeType
198
+ class FakeAttributeType
197
199
  def initialize(model, attribute_name)
198
200
  @model = model
199
201
  @attribute_name = attribute_name
200
202
  end
201
203
 
202
204
  def coder
203
- if model.respond_to?(:serialized_attributes)
204
- ActiveSupport::Deprecation.silence do
205
- model.serialized_attributes[attribute_name.to_s]
206
- end
207
- end
205
+ nil
208
206
  end
209
207
 
210
208
  private