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.
- 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
|