shoulda-matchers 4.1.2 → 4.5.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/{MIT-LICENSE → LICENSE} +1 -1
- data/README.md +168 -86
- data/lib/shoulda/matchers/action_controller/callback_matcher.rb +4 -2
- data/lib/shoulda/matchers/action_controller/filter_param_matcher.rb +3 -2
- data/lib/shoulda/matchers/action_controller/permit_matcher.rb +26 -21
- data/lib/shoulda/matchers/action_controller/redirect_to_matcher.rb +6 -8
- data/lib/shoulda/matchers/action_controller/render_template_matcher.rb +6 -8
- data/lib/shoulda/matchers/action_controller/render_with_layout_matcher.rb +16 -13
- data/lib/shoulda/matchers/action_controller/rescue_from_matcher.rb +2 -1
- data/lib/shoulda/matchers/action_controller/route_matcher.rb +5 -6
- data/lib/shoulda/matchers/action_controller/route_params.rb +1 -1
- data/lib/shoulda/matchers/action_controller/set_session_or_flash_matcher.rb +19 -13
- data/lib/shoulda/matchers/active_model/allow_mass_assignment_of_matcher.rb +18 -16
- data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +31 -29
- data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_changed_value_error.rb +1 -1
- data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setter.rb +5 -5
- data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setter_and_validator.rb +2 -2
- data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters.rb +1 -1
- data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters_and_validators.rb +1 -1
- data/lib/shoulda/matchers/active_model/disallow_value_matcher.rb +1 -1
- data/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb +51 -25
- data/lib/shoulda/matchers/active_model/helpers.rb +1 -1
- data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +32 -30
- data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +1 -1
- data/lib/shoulda/matchers/active_model/qualifiers/ignoring_interference_by_writer.rb +1 -1
- data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +9 -1
- data/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +2 -2
- data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +9 -8
- data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +28 -46
- data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +33 -9
- data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +71 -26
- data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +2 -2
- data/lib/shoulda/matchers/active_model/validation_matcher.rb +31 -6
- data/lib/shoulda/matchers/active_model/validation_matcher/build_description.rb +2 -4
- data/lib/shoulda/matchers/active_model/validation_message_finder.rb +2 -4
- data/lib/shoulda/matchers/active_model/validator.rb +3 -3
- data/lib/shoulda/matchers/active_record.rb +26 -23
- data/lib/shoulda/matchers/active_record/accept_nested_attributes_for_matcher.rb +6 -3
- data/lib/shoulda/matchers/active_record/association_matcher.rb +100 -44
- data/lib/shoulda/matchers/active_record/association_matchers/counter_cache_matcher.rb +5 -2
- data/lib/shoulda/matchers/active_record/association_matchers/dependent_matcher.rb +4 -4
- data/lib/shoulda/matchers/active_record/association_matchers/inverse_of_matcher.rb +1 -1
- data/lib/shoulda/matchers/active_record/association_matchers/join_table_matcher.rb +11 -6
- data/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +14 -15
- data/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +30 -8
- data/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb +23 -5
- data/lib/shoulda/matchers/active_record/association_matchers/optional_matcher.rb +3 -3
- data/lib/shoulda/matchers/active_record/association_matchers/order_matcher.rb +1 -1
- data/lib/shoulda/matchers/active_record/association_matchers/required_matcher.rb +3 -3
- data/lib/shoulda/matchers/active_record/association_matchers/source_matcher.rb +3 -2
- data/lib/shoulda/matchers/active_record/association_matchers/through_matcher.rb +7 -5
- data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +8 -8
- data/lib/shoulda/matchers/active_record/have_attached_matcher.rb +185 -0
- data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +40 -18
- data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +1 -1
- data/lib/shoulda/matchers/active_record/have_implicit_order_column.rb +106 -0
- data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +12 -10
- data/lib/shoulda/matchers/active_record/have_rich_text_matcher.rb +83 -0
- data/lib/shoulda/matchers/active_record/have_secure_token_matcher.rb +30 -9
- data/lib/shoulda/matchers/active_record/serialize_matcher.rb +13 -9
- data/lib/shoulda/matchers/active_record/uniqueness.rb +1 -1
- data/lib/shoulda/matchers/active_record/uniqueness/test_model_creator.rb +1 -3
- data/lib/shoulda/matchers/active_record/uniqueness/test_models.rb +0 -2
- data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +80 -73
- data/lib/shoulda/matchers/doublespeak.rb +2 -1
- data/lib/shoulda/matchers/doublespeak/double.rb +1 -1
- data/lib/shoulda/matchers/doublespeak/double_collection.rb +3 -3
- data/lib/shoulda/matchers/doublespeak/double_implementation_registry.rb +8 -5
- data/lib/shoulda/matchers/doublespeak/object_double.rb +1 -1
- data/lib/shoulda/matchers/doublespeak/stub_implementation.rb +1 -5
- data/lib/shoulda/matchers/doublespeak/world.rb +2 -2
- data/lib/shoulda/matchers/error.rb +1 -1
- data/lib/shoulda/matchers/independent.rb +0 -1
- data/lib/shoulda/matchers/independent/delegate_method_matcher.rb +14 -13
- data/lib/shoulda/matchers/integrations/configuration.rb +1 -1
- data/lib/shoulda/matchers/integrations/libraries/action_controller.rb +1 -1
- data/lib/shoulda/matchers/integrations/libraries/rails.rb +2 -2
- data/lib/shoulda/matchers/integrations/test_frameworks/active_support_test_case.rb +1 -1
- data/lib/shoulda/matchers/integrations/test_frameworks/minitest_4.rb +1 -1
- data/lib/shoulda/matchers/integrations/test_frameworks/minitest_5.rb +1 -1
- data/lib/shoulda/matchers/integrations/test_frameworks/missing_test_framework.rb +1 -1
- data/lib/shoulda/matchers/integrations/test_frameworks/test_unit.rb +1 -1
- data/lib/shoulda/matchers/rails_shim.rb +10 -21
- data/lib/shoulda/matchers/util.rb +16 -4
- data/lib/shoulda/matchers/util/word_wrap.rb +8 -8
- data/lib/shoulda/matchers/version.rb +1 -1
- data/lib/shoulda/matchers/warn.rb +3 -3
- data/shoulda-matchers.gemspec +10 -7
- metadata +11 -9
- data/lib/shoulda/matchers/independent/delegate_method_matcher/stubbed_target.rb +0 -37
@@ -18,7 +18,7 @@ module Shoulda
|
|
18
18
|
@reflector = reflector
|
19
19
|
end
|
20
20
|
|
21
|
-
def matches?(
|
21
|
+
def matches?(_subject)
|
22
22
|
join_table_option_correct? &&
|
23
23
|
join_table_exists? &&
|
24
24
|
join_table_has_correct_columns?
|
@@ -26,10 +26,14 @@ module Shoulda
|
|
26
26
|
|
27
27
|
def join_table_option_correct?
|
28
28
|
if options.key?(:join_table_name)
|
29
|
-
if option_verifier.correct_for_string?(
|
29
|
+
if option_verifier.correct_for_string?(
|
30
|
+
:join_table,
|
31
|
+
options[:join_table_name],
|
32
|
+
)
|
30
33
|
true
|
31
34
|
else
|
32
|
-
@failure_message = "#{name} should use
|
35
|
+
@failure_message = "#{name} should use"\
|
36
|
+
" #{options[:join_table_name].inspect} for :join_table option"
|
33
37
|
false
|
34
38
|
end
|
35
39
|
else
|
@@ -38,7 +42,8 @@ module Shoulda
|
|
38
42
|
end
|
39
43
|
|
40
44
|
def join_table_exists?
|
41
|
-
if RailsShim.tables_and_views(connection).
|
45
|
+
if RailsShim.tables_and_views(connection).
|
46
|
+
include?(join_table_name.to_s)
|
42
47
|
true
|
43
48
|
else
|
44
49
|
@failure_message = missing_table_message
|
@@ -64,8 +69,8 @@ module Shoulda
|
|
64
69
|
delegate :foreign_key, :association_foreign_key, to: :reflector
|
65
70
|
|
66
71
|
def missing_columns
|
67
|
-
@
|
68
|
-
|
72
|
+
@_missing_columns ||= expected_join_table_columns.reject do |key|
|
73
|
+
actual_join_table_columns.include?(key.to_s)
|
69
74
|
end
|
70
75
|
end
|
71
76
|
|
@@ -26,21 +26,16 @@ module Shoulda
|
|
26
26
|
|
27
27
|
def join_table_name
|
28
28
|
join_table_name =
|
29
|
-
|
30
|
-
has_and_belongs_to_many_name_table_name
|
31
|
-
else
|
32
|
-
reflection.join_table
|
33
|
-
end
|
34
|
-
|
29
|
+
has_and_belongs_to_many_name_table_name || reflection.join_table
|
35
30
|
join_table_name.to_s
|
36
31
|
end
|
37
32
|
|
38
|
-
def association_relation
|
33
|
+
def association_relation(related_instance)
|
39
34
|
relation = associated_class.all
|
40
35
|
|
41
36
|
if reflection.scope
|
42
37
|
# Source: AR::Associations::AssociationScope#eval_scope
|
43
|
-
relation.instance_exec(
|
38
|
+
relation.instance_exec(related_instance, &reflection.scope)
|
44
39
|
else
|
45
40
|
relation
|
46
41
|
end
|
@@ -65,20 +60,24 @@ module Shoulda
|
|
65
60
|
end
|
66
61
|
end
|
67
62
|
|
63
|
+
def validate_inverse_of_through_association!
|
64
|
+
if through?
|
65
|
+
reflection.check_validity!
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def has_and_belongs_to_many_name
|
70
|
+
reflection.options[:through]
|
71
|
+
end
|
72
|
+
|
68
73
|
protected
|
69
74
|
|
70
75
|
attr_reader :reflection, :subject
|
71
76
|
|
72
77
|
private
|
73
78
|
|
74
|
-
def has_and_belongs_to_many_name
|
75
|
-
reflection.options[:through]
|
76
|
-
end
|
77
|
-
|
78
79
|
def has_and_belongs_to_many_name_table_name
|
79
|
-
|
80
|
-
has_and_belongs_to_many_reflection.table_name
|
81
|
-
end
|
80
|
+
has_and_belongs_to_many_reflection&.table_name
|
82
81
|
end
|
83
82
|
|
84
83
|
def has_and_belongs_to_many_reflection
|
@@ -4,17 +4,34 @@ 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
|
19
36
|
|
20
37
|
def reflect_on_association(name)
|
@@ -31,9 +48,12 @@ module Shoulda
|
|
31
48
|
|
32
49
|
def build_relation_with_clause(name, value)
|
33
50
|
case name
|
34
|
-
|
35
|
-
|
36
|
-
|
51
|
+
when :conditions
|
52
|
+
associated_class.where(value)
|
53
|
+
when :order
|
54
|
+
associated_class.order(value)
|
55
|
+
else
|
56
|
+
raise ArgumentError, "Unknown clause '#{name}'"
|
37
57
|
end
|
38
58
|
end
|
39
59
|
|
@@ -42,7 +62,9 @@ module Shoulda
|
|
42
62
|
when :conditions
|
43
63
|
relation.where_values_hash
|
44
64
|
when :order
|
45
|
-
relation.order_values.map
|
65
|
+
relation.order_values.map do |value|
|
66
|
+
value_as_sql(value)
|
67
|
+
end.join(', ')
|
46
68
|
else
|
47
69
|
raise ArgumentError, "Unknown clause '#{name}'"
|
48
70
|
end
|
@@ -6,7 +6,12 @@ module Shoulda
|
|
6
6
|
class OptionVerifier
|
7
7
|
delegate :reflection, to: :reflector
|
8
8
|
|
9
|
-
|
9
|
+
DEFAULT_VALUE_OF_OPTIONS = {
|
10
|
+
has_many: {
|
11
|
+
validate: true,
|
12
|
+
},
|
13
|
+
}.freeze
|
14
|
+
RELATION_OPTIONS = [:conditions, :order].freeze
|
10
15
|
|
11
16
|
def initialize(reflector)
|
12
17
|
@reflector = reflector
|
@@ -40,7 +45,7 @@ module Shoulda
|
|
40
45
|
else
|
41
46
|
type_cast_expected_value = type_cast(
|
42
47
|
type,
|
43
|
-
expected_value_for(type, name, expected_value)
|
48
|
+
expected_value_for(type, name, expected_value),
|
44
49
|
)
|
45
50
|
actual_value = type_cast(type, actual_value_for(name))
|
46
51
|
type_cast_expected_value == actual_value
|
@@ -55,7 +60,7 @@ module Shoulda
|
|
55
60
|
if respond_to?(method_name, true)
|
56
61
|
__send__(method_name)
|
57
62
|
else
|
58
|
-
|
63
|
+
actual_value_for_option(name)
|
59
64
|
end
|
60
65
|
end
|
61
66
|
end
|
@@ -94,7 +99,7 @@ module Shoulda
|
|
94
99
|
|
95
100
|
def expected_value_for_constant(name)
|
96
101
|
namespace = Shoulda::Matchers::Util.deconstantize(
|
97
|
-
reflector.model_class.to_s
|
102
|
+
reflector.model_class.to_s,
|
98
103
|
)
|
99
104
|
|
100
105
|
["#{namespace}::#{name}", name].each do |path|
|
@@ -107,12 +112,25 @@ module Shoulda
|
|
107
112
|
end
|
108
113
|
|
109
114
|
def actual_value_for_relation_clause(name)
|
110
|
-
reflector.extract_relation_clause_from(
|
115
|
+
reflector.extract_relation_clause_from(
|
116
|
+
reflector.association_relation,
|
117
|
+
name,
|
118
|
+
)
|
111
119
|
end
|
112
120
|
|
113
121
|
def actual_value_for_class_name
|
114
122
|
reflector.associated_class
|
115
123
|
end
|
124
|
+
|
125
|
+
def actual_value_for_option(name)
|
126
|
+
option_value = reflection.options[name]
|
127
|
+
|
128
|
+
if option_value.nil?
|
129
|
+
DEFAULT_VALUE_OF_OPTIONS.dig(reflection.macro, name)
|
130
|
+
else
|
131
|
+
option_value
|
132
|
+
end
|
133
|
+
end
|
116
134
|
end
|
117
135
|
end
|
118
136
|
end
|
@@ -32,9 +32,9 @@ module Shoulda
|
|
32
32
|
end
|
33
33
|
|
34
34
|
missing_option << (
|
35
|
-
'fail validation if '
|
36
|
-
":#{attribute_name} is unset; i.e., either the association "
|
37
|
-
'should have been defined with `optional: '
|
35
|
+
'fail validation if '\
|
36
|
+
":#{attribute_name} is unset; i.e., either the association "\
|
37
|
+
'should have been defined with `optional: '\
|
38
38
|
"#{optional.inspect}`, or there "
|
39
39
|
)
|
40
40
|
|
@@ -33,9 +33,9 @@ module Shoulda
|
|
33
33
|
end
|
34
34
|
|
35
35
|
missing_option << (
|
36
|
-
'fail validation if '
|
37
|
-
":#{attribute_name} is unset; i.e., either the association "
|
38
|
-
'should have been defined with `required: '
|
36
|
+
'fail validation if '\
|
37
|
+
":#{attribute_name} is unset; i.e., either the association "\
|
38
|
+
'should have been defined with `required: '\
|
39
39
|
"#{required.inspect}`, or there "
|
40
40
|
)
|
41
41
|
|
@@ -22,7 +22,8 @@ module Shoulda
|
|
22
22
|
if option_verifier.correct_for_string?(:source, source)
|
23
23
|
true
|
24
24
|
else
|
25
|
-
self.missing_option =
|
25
|
+
self.missing_option =
|
26
|
+
"#{name} should have #{source} as source option"
|
26
27
|
false
|
27
28
|
end
|
28
29
|
end
|
@@ -32,7 +33,7 @@ module Shoulda
|
|
32
33
|
attr_accessor :subject, :source, :name
|
33
34
|
|
34
35
|
def option_verifier
|
35
|
-
@
|
36
|
+
@_option_verifier ||= OptionVerifier.new(subject)
|
36
37
|
end
|
37
38
|
end
|
38
39
|
end
|
@@ -29,13 +29,14 @@ module Shoulda
|
|
29
29
|
if through_reflection.present?
|
30
30
|
true
|
31
31
|
else
|
32
|
-
self.missing_option =
|
32
|
+
self.missing_option =
|
33
|
+
"#{name} does not have any relationship to #{through}"
|
33
34
|
false
|
34
35
|
end
|
35
36
|
end
|
36
37
|
|
37
38
|
def through_reflection
|
38
|
-
@
|
39
|
+
@_through_reflection ||= subject.reflect_on_association(through)
|
39
40
|
end
|
40
41
|
|
41
42
|
def through_association_correct?
|
@@ -43,8 +44,9 @@ module Shoulda
|
|
43
44
|
true
|
44
45
|
else
|
45
46
|
self.missing_option =
|
46
|
-
"Expected #{name} to have #{name} through #{through}, "
|
47
|
-
|
47
|
+
"Expected #{name} to have #{name} through #{through}, "\
|
48
|
+
'but got it through ' +
|
49
|
+
option_verifier.actual_value_for(:through).to_s
|
48
50
|
false
|
49
51
|
end
|
50
52
|
end
|
@@ -54,7 +56,7 @@ module Shoulda
|
|
54
56
|
attr_accessor :through, :name, :subject
|
55
57
|
|
56
58
|
def option_verifier
|
57
|
-
@
|
59
|
+
@_option_verifier ||= OptionVerifier.new(subject)
|
58
60
|
end
|
59
61
|
end
|
60
62
|
end
|
@@ -237,7 +237,7 @@ module Shoulda
|
|
237
237
|
"Expected #{model} to #{expectation}, but "
|
238
238
|
end
|
239
239
|
|
240
|
-
message << failure_message_continuation
|
240
|
+
message << "#{failure_message_continuation}."
|
241
241
|
|
242
242
|
Shoulda::Matchers.word_wrap(message)
|
243
243
|
end
|
@@ -252,7 +252,7 @@ module Shoulda
|
|
252
252
|
attr_reader :attribute_name, :options, :record,
|
253
253
|
:failure_message_continuation
|
254
254
|
|
255
|
-
def expectation
|
255
|
+
def expectation # rubocop:disable Metrics/MethodLength
|
256
256
|
if enum_defined?
|
257
257
|
expectation = "#{simple_description} backed by "
|
258
258
|
expectation << Shoulda::Matchers::Util.a_or_an(expected_column_type)
|
@@ -358,8 +358,8 @@ module Shoulda
|
|
358
358
|
true
|
359
359
|
else
|
360
360
|
@failure_message_continuation =
|
361
|
-
"However, #{attribute_name.inspect} is "
|
362
|
-
Shoulda::Matchers::Util.a_or_an(column.type)
|
361
|
+
"However, #{attribute_name.inspect} is "\
|
362
|
+
"#{Shoulda::Matchers::Util.a_or_an(column.type)}"\
|
363
363
|
' column'
|
364
364
|
false
|
365
365
|
end
|
@@ -421,9 +421,9 @@ module Shoulda
|
|
421
421
|
def expected_prefix
|
422
422
|
if options.include?(:prefix)
|
423
423
|
if options[:prefix] == true
|
424
|
-
attribute_name
|
424
|
+
attribute_name
|
425
425
|
else
|
426
|
-
options[:prefix]
|
426
|
+
options[:prefix]
|
427
427
|
end
|
428
428
|
end
|
429
429
|
end
|
@@ -431,9 +431,9 @@ module Shoulda
|
|
431
431
|
def expected_suffix
|
432
432
|
if options.include?(:suffix)
|
433
433
|
if options[:suffix] == true
|
434
|
-
attribute_name
|
434
|
+
attribute_name
|
435
435
|
else
|
436
|
-
options[:suffix]
|
436
|
+
options[:suffix]
|
437
437
|
end
|
438
438
|
end
|
439
439
|
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
module Shoulda
|
2
|
+
module Matchers
|
3
|
+
module ActiveRecord
|
4
|
+
# The `have_one_attached` matcher tests usage of the
|
5
|
+
# `has_one_attached` macro.
|
6
|
+
#
|
7
|
+
# #### Example
|
8
|
+
#
|
9
|
+
# class User < ApplicationRecord
|
10
|
+
# has_one_attached :avatar
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# # RSpec
|
14
|
+
# RSpec.describe User, type: :model do
|
15
|
+
# it { should have_one_attached(:avatar) }
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# # Minitest (Shoulda)
|
19
|
+
# class UserTest < ActiveSupport::TestCase
|
20
|
+
# should have_one_attached(:avatar)
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# @return [HaveAttachedMatcher]
|
24
|
+
#
|
25
|
+
def have_one_attached(name)
|
26
|
+
HaveAttachedMatcher.new(:one, name)
|
27
|
+
end
|
28
|
+
|
29
|
+
# The `have_many_attached` matcher tests usage of the
|
30
|
+
# `has_many_attached` macro.
|
31
|
+
#
|
32
|
+
# #### Example
|
33
|
+
#
|
34
|
+
# class Message < ApplicationRecord
|
35
|
+
# has_many_attached :images
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# # RSpec
|
39
|
+
# RSpec.describe Message, type: :model do
|
40
|
+
# it { should have_many_attached(:images) }
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# # Minitest (Shoulda)
|
44
|
+
# class MessageTest < ActiveSupport::TestCase
|
45
|
+
# should have_many_attached(:images)
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# @return [HaveAttachedMatcher]
|
49
|
+
#
|
50
|
+
def have_many_attached(name)
|
51
|
+
HaveAttachedMatcher.new(:many, name)
|
52
|
+
end
|
53
|
+
|
54
|
+
# @private
|
55
|
+
class HaveAttachedMatcher
|
56
|
+
attr_reader :name
|
57
|
+
|
58
|
+
def initialize(macro, name)
|
59
|
+
@macro = macro
|
60
|
+
@name = name
|
61
|
+
end
|
62
|
+
|
63
|
+
def description
|
64
|
+
"have a has_#{macro}_attached called #{name}"
|
65
|
+
end
|
66
|
+
|
67
|
+
def failure_message
|
68
|
+
<<-MESSAGE
|
69
|
+
Expected #{expectation}, but this could not be proved.
|
70
|
+
#{@failure}
|
71
|
+
MESSAGE
|
72
|
+
end
|
73
|
+
|
74
|
+
def failure_message_when_negated
|
75
|
+
<<-MESSAGE
|
76
|
+
Did not expect #{expectation}, but it does.
|
77
|
+
MESSAGE
|
78
|
+
end
|
79
|
+
|
80
|
+
def expectation
|
81
|
+
"#{model_class.name} to #{description}"
|
82
|
+
end
|
83
|
+
|
84
|
+
def matches?(subject)
|
85
|
+
@subject = subject
|
86
|
+
reader_attribute_exists? &&
|
87
|
+
writer_attribute_exists? &&
|
88
|
+
attachments_association_exists? &&
|
89
|
+
blobs_association_exists? &&
|
90
|
+
eager_loading_scope_exists?
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
attr_reader :subject, :macro
|
96
|
+
|
97
|
+
def reader_attribute_exists?
|
98
|
+
if subject.respond_to?(name)
|
99
|
+
true
|
100
|
+
else
|
101
|
+
@failure = "#{model_class.name} does not have a :#{name} method."
|
102
|
+
false
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def writer_attribute_exists?
|
107
|
+
if subject.respond_to?("#{name}=")
|
108
|
+
true
|
109
|
+
else
|
110
|
+
@failure = "#{model_class.name} does not have a :#{name}= method."
|
111
|
+
false
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def attachments_association_exists?
|
116
|
+
if attachments_association_matcher.matches?(subject)
|
117
|
+
true
|
118
|
+
else
|
119
|
+
@failure = attachments_association_matcher.failure_message
|
120
|
+
false
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def attachments_association_matcher
|
125
|
+
@_attachments_association_matcher ||=
|
126
|
+
AssociationMatcher.new(
|
127
|
+
:"has_#{macro}",
|
128
|
+
attachments_association_name,
|
129
|
+
).
|
130
|
+
conditions(name: name).
|
131
|
+
class_name('ActiveStorage::Attachment').
|
132
|
+
inverse_of(:record)
|
133
|
+
end
|
134
|
+
|
135
|
+
def attachments_association_name
|
136
|
+
case macro
|
137
|
+
when :one then "#{name}_attachment"
|
138
|
+
when :many then "#{name}_attachments"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def blobs_association_exists?
|
143
|
+
if blobs_association_matcher.matches?(subject)
|
144
|
+
true
|
145
|
+
else
|
146
|
+
@failure = blobs_association_matcher.failure_message
|
147
|
+
false
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def blobs_association_matcher
|
152
|
+
@_blobs_association_matcher ||=
|
153
|
+
AssociationMatcher.new(
|
154
|
+
:"has_#{macro}",
|
155
|
+
blobs_association_name,
|
156
|
+
).
|
157
|
+
through(attachments_association_name).
|
158
|
+
class_name('ActiveStorage::Blob').
|
159
|
+
source(:blob)
|
160
|
+
end
|
161
|
+
|
162
|
+
def blobs_association_name
|
163
|
+
case macro
|
164
|
+
when :one then "#{name}_blob"
|
165
|
+
when :many then "#{name}_blobs"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def eager_loading_scope_exists?
|
170
|
+
if model_class.respond_to?("with_attached_#{name}")
|
171
|
+
true
|
172
|
+
else
|
173
|
+
@failure = "#{model_class.name} does not have a " \
|
174
|
+
":with_attached_#{name} scope."
|
175
|
+
false
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def model_class
|
180
|
+
subject.class
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|