shoulda-matchers 4.3.0 → 4.4.0
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 +4 -4
- data/README.md +157 -77
- data/lib/shoulda/matchers.rb +13 -12
- data/lib/shoulda/matchers/action_controller.rb +13 -13
- data/lib/shoulda/matchers/active_model.rb +15 -26
- data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +9 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers.rb +5 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +5 -1
- data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +2 -21
- data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +27 -3
- data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +32 -0
- data/lib/shoulda/matchers/active_model/validation_matcher.rb +27 -0
- data/lib/shoulda/matchers/active_record.rb +13 -23
- data/lib/shoulda/matchers/active_record/association_matcher.rb +20 -4
- data/lib/shoulda/matchers/active_record/association_matchers.rb +12 -0
- data/lib/shoulda/matchers/active_record/association_matchers/join_table_matcher.rb +2 -2
- data/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +12 -6
- data/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +20 -3
- data/lib/shoulda/matchers/active_record/have_attached_matcher.rb +147 -0
- data/lib/shoulda/matchers/active_record/have_implicit_order_column.rb +106 -0
- data/lib/shoulda/matchers/active_record/have_secure_token_matcher.rb +28 -9
- data/lib/shoulda/matchers/active_record/uniqueness.rb +5 -5
- data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +2 -2
- data/lib/shoulda/matchers/doublespeak.rb +8 -7
- data/lib/shoulda/matchers/independent.rb +0 -2
- data/lib/shoulda/matchers/independent/delegate_method_matcher.rb +3 -0
- data/lib/shoulda/matchers/integrations.rb +6 -6
- data/lib/shoulda/matchers/integrations/test_frameworks.rb +4 -2
- data/lib/shoulda/matchers/rails_shim.rb +4 -0
- data/lib/shoulda/matchers/util.rb +9 -4
- data/lib/shoulda/matchers/util/word_wrap.rb +1 -1
- data/lib/shoulda/matchers/version.rb +1 -1
- metadata +8 -8
- data/MIT-LICENSE +0 -22
- 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
|
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(
|
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
|
8
|
-
:
|
9
|
-
:association_foreign_key,
|
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
|
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.
|
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
|