shoulda-matchers 4.3.0 → 4.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +157 -77
  3. data/lib/shoulda/matchers.rb +13 -12
  4. data/lib/shoulda/matchers/action_controller.rb +13 -13
  5. data/lib/shoulda/matchers/active_model.rb +15 -26
  6. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +9 -0
  7. data/lib/shoulda/matchers/active_model/numericality_matchers.rb +5 -0
  8. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +5 -1
  9. data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +2 -21
  10. data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +27 -3
  11. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +32 -0
  12. data/lib/shoulda/matchers/active_model/validation_matcher.rb +27 -0
  13. data/lib/shoulda/matchers/active_record.rb +13 -23
  14. data/lib/shoulda/matchers/active_record/association_matcher.rb +20 -4
  15. data/lib/shoulda/matchers/active_record/association_matchers.rb +12 -0
  16. data/lib/shoulda/matchers/active_record/association_matchers/join_table_matcher.rb +2 -2
  17. data/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +12 -6
  18. data/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +20 -3
  19. data/lib/shoulda/matchers/active_record/have_attached_matcher.rb +147 -0
  20. data/lib/shoulda/matchers/active_record/have_implicit_order_column.rb +106 -0
  21. data/lib/shoulda/matchers/active_record/have_secure_token_matcher.rb +28 -9
  22. data/lib/shoulda/matchers/active_record/uniqueness.rb +5 -5
  23. data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +2 -2
  24. data/lib/shoulda/matchers/doublespeak.rb +8 -7
  25. data/lib/shoulda/matchers/independent.rb +0 -2
  26. data/lib/shoulda/matchers/independent/delegate_method_matcher.rb +3 -0
  27. data/lib/shoulda/matchers/integrations.rb +6 -6
  28. data/lib/shoulda/matchers/integrations/test_frameworks.rb +4 -2
  29. data/lib/shoulda/matchers/rails_shim.rb +4 -0
  30. data/lib/shoulda/matchers/util.rb +9 -4
  31. data/lib/shoulda/matchers/util/word_wrap.rb +1 -1
  32. data/lib/shoulda/matchers/version.rb +1 -1
  33. metadata +8 -8
  34. data/MIT-LICENSE +0 -22
  35. data/lib/shoulda/matchers/independent/delegate_method_matcher/stubbed_target.rb +0 -37
@@ -29,7 +29,7 @@ module Shoulda
29
29
  if option_verifier.correct_for_string?(:join_table, options[:join_table_name])
30
30
  true
31
31
  else
32
- @failure_message = "#{name} should use '#{options[:join_table_name]}' for :join_table option"
32
+ @failure_message = "#{name} should use #{options[:join_table_name].inspect} for :join_table option"
33
33
  false
34
34
  end
35
35
  else
@@ -38,7 +38,7 @@ module Shoulda
38
38
  end
39
39
 
40
40
  def join_table_exists?
41
- if RailsShim.tables_and_views(connection).include?(join_table_name)
41
+ if RailsShim.tables_and_views(connection).include?(join_table_name.to_s)
42
42
  true
43
43
  else
44
44
  @failure_message = missing_table_message
@@ -35,12 +35,12 @@ module Shoulda
35
35
  join_table_name.to_s
36
36
  end
37
37
 
38
- def association_relation
38
+ def association_relation(related_instance)
39
39
  relation = associated_class.all
40
40
 
41
41
  if reflection.scope
42
42
  # Source: AR::Associations::AssociationScope#eval_scope
43
- relation.instance_exec(subject, &reflection.scope)
43
+ relation.instance_exec(related_instance, &reflection.scope)
44
44
  else
45
45
  relation
46
46
  end
@@ -65,16 +65,22 @@ module Shoulda
65
65
  end
66
66
  end
67
67
 
68
+ def validate_inverse_of_through_association!
69
+ if through?
70
+ reflection.check_validity!
71
+ end
72
+ end
73
+
74
+ def has_and_belongs_to_many_name
75
+ reflection.options[:through]
76
+ end
77
+
68
78
  protected
69
79
 
70
80
  attr_reader :reflection, :subject
71
81
 
72
82
  private
73
83
 
74
- def has_and_belongs_to_many_name
75
- reflection.options[:through]
76
- end
77
-
78
84
  def has_and_belongs_to_many_name_table_name
79
85
  if has_and_belongs_to_many_reflection
80
86
  has_and_belongs_to_many_reflection.table_name
@@ -4,15 +4,32 @@ module Shoulda
4
4
  module AssociationMatchers
5
5
  # @private
6
6
  class ModelReflector
7
- delegate :associated_class, :through?, :join_table_name,
8
- :association_relation, :polymorphic?, :foreign_key,
9
- :association_foreign_key, to: :reflection
7
+ delegate(
8
+ :associated_class,
9
+ :association_foreign_key,
10
+ :foreign_key,
11
+ :has_and_belongs_to_many_name,
12
+ :join_table_name,
13
+ :polymorphic?,
14
+ :validate_inverse_of_through_association!,
15
+ to: :reflection
16
+ )
17
+
18
+ delegate(
19
+ :through?,
20
+ to: :reflection,
21
+ allow_nil: true
22
+ )
10
23
 
11
24
  def initialize(subject, name)
12
25
  @subject = subject
13
26
  @name = name
14
27
  end
15
28
 
29
+ def association_relation
30
+ reflection.association_relation(subject)
31
+ end
32
+
16
33
  def reflection
17
34
  @reflection ||= reflect_on_association(name)
18
35
  end
@@ -0,0 +1,147 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module ActiveRecord
4
+ def have_one_attached(name)
5
+ HaveAttachedMatcher.new(:one, name)
6
+ end
7
+
8
+ def have_many_attached(name)
9
+ HaveAttachedMatcher.new(:many, name)
10
+ end
11
+
12
+ # @private
13
+ class HaveAttachedMatcher
14
+ attr_reader :name
15
+
16
+ def initialize(macro, name)
17
+ @macro = macro
18
+ @name = name
19
+ end
20
+
21
+ def description
22
+ "have a has_#{macro}_attached called #{name}"
23
+ end
24
+
25
+ def failure_message
26
+ <<-MESSAGE
27
+ Expected #{expectation}, but this could not be proved.
28
+ #{@failure}
29
+ MESSAGE
30
+ end
31
+
32
+ def failure_message_when_negated
33
+ <<-MESSAGE
34
+ Did not expect #{expectation}, but it does.
35
+ MESSAGE
36
+ end
37
+
38
+ def expectation
39
+ "#{model_class.name} to #{description}"
40
+ end
41
+
42
+ def matches?(subject)
43
+ @subject = subject
44
+ reader_attribute_exists? &&
45
+ writer_attribute_exists? &&
46
+ attachments_association_exists? &&
47
+ blobs_association_exists? &&
48
+ eager_loading_scope_exists?
49
+ end
50
+
51
+ private
52
+
53
+ attr_reader :subject, :macro
54
+
55
+ def reader_attribute_exists?
56
+ if subject.respond_to?(name)
57
+ true
58
+ else
59
+ @failure = "#{model_class.name} does not have a :#{name} method."
60
+ false
61
+ end
62
+ end
63
+
64
+ def writer_attribute_exists?
65
+ if subject.respond_to?("#{name}=")
66
+ true
67
+ else
68
+ @failure = "#{model_class.name} does not have a :#{name}= method."
69
+ false
70
+ end
71
+ end
72
+
73
+ def attachments_association_exists?
74
+ if attachments_association_matcher.matches?(subject)
75
+ true
76
+ else
77
+ @failure = attachments_association_matcher.failure_message
78
+ false
79
+ end
80
+ end
81
+
82
+ def attachments_association_matcher
83
+ @_attachments_association_matcher ||=
84
+ AssociationMatcher.new(
85
+ :"has_#{macro}",
86
+ attachments_association_name,
87
+ ).
88
+ conditions(name: name).
89
+ class_name('ActiveStorage::Attachment').
90
+ inverse_of(:record)
91
+ end
92
+
93
+ def attachments_association_name
94
+ case macro
95
+ when :one then
96
+ "#{name}_attachment"
97
+ when :many then
98
+ "#{name}_attachments"
99
+ end
100
+ end
101
+
102
+ def blobs_association_exists?
103
+ if blobs_association_matcher.matches?(subject)
104
+ true
105
+ else
106
+ @failure = blobs_association_matcher.failure_message
107
+ false
108
+ end
109
+ end
110
+
111
+ def blobs_association_matcher
112
+ @_blobs_association_matcher ||=
113
+ AssociationMatcher.new(
114
+ :"has_#{macro}",
115
+ blobs_association_name,
116
+ ).
117
+ through(attachments_association_name).
118
+ class_name('ActiveStorage::Blob').
119
+ source(:blob)
120
+ end
121
+
122
+ def blobs_association_name
123
+ case macro
124
+ when :one then
125
+ "#{name}_blob"
126
+ when :many then
127
+ "#{name}_blobs"
128
+ end
129
+ end
130
+
131
+ def eager_loading_scope_exists?
132
+ if model_class.respond_to?("with_attached_#{name}")
133
+ true
134
+ else
135
+ @failure = "#{model_class.name} does not have a " \
136
+ ":with_attached_#{name} scope."
137
+ false
138
+ end
139
+ end
140
+
141
+ def model_class
142
+ subject.class
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -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
@@ -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
@@ -1,14 +1,14 @@
1
1
  module Shoulda
2
2
  module Matchers
3
- module ActiveModel
3
+ module ActiveRecord
4
4
  # @private
5
5
  module Uniqueness
6
+ autoload :Model, 'shoulda/matchers/active_record/uniqueness/model'
7
+ autoload :Namespace, 'shoulda/matchers/active_record/uniqueness/namespace'
8
+ autoload :TestModelCreator, 'shoulda/matchers/active_record/uniqueness/test_model_creator'
9
+ autoload :TestModels, 'shoulda/matchers/active_record/uniqueness/test_models'
6
10
  end
7
11
  end
8
12
  end
9
13
  end
10
14
 
11
- require 'shoulda/matchers/active_record/uniqueness/model'
12
- require 'shoulda/matchers/active_record/uniqueness/namespace'
13
- require 'shoulda/matchers/active_record/uniqueness/test_model_creator'
14
- require 'shoulda/matchers/active_record/uniqueness/test_models'
@@ -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