shoulda-matchers 3.0.1 → 3.1.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.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +3 -3
  4. data/CONTRIBUTING.md +60 -28
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +15 -12
  7. data/NEWS.md +111 -0
  8. data/README.md +94 -6
  9. data/Rakefile +10 -8
  10. data/custom_plan.rb +88 -0
  11. data/gemfiles/4.0.0.gemfile +1 -0
  12. data/gemfiles/4.0.0.gemfile.lock +21 -18
  13. data/gemfiles/4.0.1.gemfile +1 -0
  14. data/gemfiles/4.0.1.gemfile.lock +21 -18
  15. data/gemfiles/4.1.gemfile +1 -0
  16. data/gemfiles/4.1.gemfile.lock +21 -18
  17. data/gemfiles/4.2.gemfile +1 -0
  18. data/gemfiles/4.2.gemfile.lock +24 -21
  19. data/lib/shoulda/matchers/action_controller/permit_matcher.rb +6 -11
  20. data/lib/shoulda/matchers/active_model.rb +10 -1
  21. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +258 -180
  22. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_changed_value_error.rb +45 -0
  23. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_does_not_exist_error.rb +23 -0
  24. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setter.rb +236 -0
  25. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setter_and_validator.rb +62 -0
  26. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters.rb +40 -0
  27. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters_and_validators.rb +48 -0
  28. data/lib/shoulda/matchers/active_model/allow_value_matcher/successful_check.rb +14 -0
  29. data/lib/shoulda/matchers/active_model/allow_value_matcher/successful_setting.rb +14 -0
  30. data/lib/shoulda/matchers/active_model/disallow_value_matcher.rb +34 -14
  31. data/lib/shoulda/matchers/active_model/helpers.rb +9 -17
  32. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +13 -6
  33. data/lib/shoulda/matchers/active_model/numericality_matchers/even_number_matcher.rb +13 -2
  34. data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +19 -35
  35. data/lib/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher.rb +13 -2
  36. data/lib/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher.rb +12 -2
  37. data/lib/shoulda/matchers/active_model/qualifiers.rb +12 -0
  38. data/lib/shoulda/matchers/active_model/qualifiers/ignore_interference_by_writer.rb +101 -0
  39. data/lib/shoulda/matchers/active_model/qualifiers/ignoring_interference_by_writer.rb +21 -0
  40. data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +30 -32
  41. data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +5 -8
  42. data/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +22 -22
  43. data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +27 -16
  44. data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +58 -15
  45. data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +22 -12
  46. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +165 -87
  47. data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +7 -9
  48. data/lib/shoulda/matchers/active_model/validation_matcher.rb +111 -49
  49. data/lib/shoulda/matchers/active_model/validation_matcher/build_description.rb +60 -0
  50. data/lib/shoulda/matchers/active_model/validator.rb +71 -52
  51. data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +19 -5
  52. data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +450 -124
  53. data/lib/shoulda/matchers/util.rb +43 -0
  54. data/lib/shoulda/matchers/util/word_wrap.rb +59 -31
  55. data/lib/shoulda/matchers/version.rb +1 -1
  56. data/script/update_gem_in_all_appraisals +1 -1
  57. data/script/update_gems_in_all_appraisals +1 -1
  58. data/spec/acceptance/multiple_libraries_integration_spec.rb +5 -2
  59. data/spec/acceptance/rails_integration_spec.rb +6 -2
  60. data/spec/spec_helper.rb +1 -3
  61. data/spec/support/acceptance/helpers/step_helpers.rb +4 -1
  62. data/spec/support/tests/current_bundle.rb +21 -7
  63. data/spec/support/unit/active_record/create_table.rb +54 -0
  64. data/spec/support/unit/attribute.rb +47 -0
  65. data/spec/support/unit/capture.rb +6 -0
  66. data/spec/support/unit/change_value.rb +111 -0
  67. data/spec/support/unit/create_model_arguments/basic.rb +135 -0
  68. data/spec/support/unit/create_model_arguments/has_many.rb +15 -0
  69. data/spec/support/unit/create_model_arguments/uniqueness_matcher.rb +74 -0
  70. data/spec/support/unit/helpers/active_record_versions.rb +1 -1
  71. data/spec/support/unit/helpers/class_builder.rb +61 -47
  72. data/spec/support/unit/helpers/database_helpers.rb +5 -3
  73. data/spec/support/unit/helpers/model_builder.rb +77 -97
  74. data/spec/support/unit/helpers/validation_matcher_scenario_helpers.rb +44 -0
  75. data/spec/support/unit/load_environment.rb +12 -0
  76. data/spec/support/unit/matchers/fail_with_message_including_matcher.rb +2 -2
  77. data/spec/support/unit/matchers/fail_with_message_matcher.rb +12 -1
  78. data/spec/support/unit/model_creation_strategies/active_model.rb +111 -0
  79. data/spec/support/unit/model_creation_strategies/active_record.rb +77 -0
  80. data/spec/support/unit/model_creators.rb +19 -0
  81. data/spec/support/unit/model_creators/active_model.rb +39 -0
  82. data/spec/support/unit/model_creators/active_record.rb +43 -0
  83. data/spec/support/unit/model_creators/active_record/has_and_belongs_to_many.rb +95 -0
  84. data/spec/support/unit/model_creators/active_record/has_many.rb +67 -0
  85. data/spec/support/unit/model_creators/active_record/uniqueness_matcher.rb +42 -0
  86. data/spec/support/unit/model_creators/basic.rb +97 -0
  87. data/spec/support/unit/rails_application.rb +1 -1
  88. data/spec/support/unit/record_validating_confirmation_builder.rb +3 -7
  89. data/spec/support/unit/shared_examples/ignoring_interference_by_writer.rb +79 -0
  90. data/spec/support/unit/validation_matcher_scenario.rb +62 -0
  91. data/spec/unit/shoulda/matchers/active_model/allow_mass_assignment_of_matcher_spec.rb +4 -0
  92. data/spec/unit/shoulda/matchers/active_model/allow_value_matcher_spec.rb +575 -140
  93. data/spec/unit/shoulda/matchers/active_model/validate_absence_of_matcher_spec.rb +115 -15
  94. data/spec/unit/shoulda/matchers/active_model/validate_acceptance_of_matcher_spec.rb +42 -4
  95. data/spec/unit/shoulda/matchers/active_model/validate_confirmation_of_matcher_spec.rb +92 -6
  96. data/spec/unit/shoulda/matchers/active_model/validate_exclusion_of_matcher_spec.rb +122 -10
  97. data/spec/unit/shoulda/matchers/active_model/validate_inclusion_of_matcher_spec.rb +306 -58
  98. data/spec/unit/shoulda/matchers/active_model/validate_length_of_matcher_spec.rb +122 -3
  99. data/spec/unit/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +805 -131
  100. data/spec/unit/shoulda/matchers/active_model/validate_presence_of_matcher_spec.rb +196 -29
  101. data/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb +82 -40
  102. data/spec/unit/shoulda/matchers/active_record/validate_uniqueness_of_matcher_spec.rb +600 -101
  103. data/spec/unit/shoulda/matchers/util/word_wrap_spec.rb +88 -33
  104. data/spec/unit_spec_helper.rb +10 -22
  105. data/zeus.json +11 -0
  106. metadata +64 -23
  107. data/lib/shoulda/matchers/active_model/strict_validator.rb +0 -51
  108. data/spec/support/unit/shared_examples/numerical_type_submatcher.rb +0 -15
  109. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/comparison_matcher_spec.rb +0 -288
  110. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/even_number_matcher_spec.rb +0 -100
  111. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher_spec.rb +0 -100
  112. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher_spec.rb +0 -100
@@ -3,13 +3,20 @@ module Shoulda
3
3
  module ActiveModel
4
4
  # @private
5
5
  class ValidationMatcher
6
- attr_reader :failure_message
6
+ include Qualifiers::IgnoringInterferenceByWriter
7
7
 
8
8
  def initialize(attribute)
9
+ super
9
10
  @attribute = attribute
10
- @strict = false
11
- @failure_message = nil
12
- @failure_message_when_negated = nil
11
+ @expects_strict = false
12
+ @subject = nil
13
+ @last_submatcher_run = nil
14
+ @expected_message = nil
15
+ @expects_custom_validation_message = false
16
+ end
17
+
18
+ def description
19
+ ValidationMatcher::BuildDescription.call(self, simple_description)
13
20
  end
14
21
 
15
22
  def on(context)
@@ -18,12 +25,25 @@ module Shoulda
18
25
  end
19
26
 
20
27
  def strict
21
- @strict = true
28
+ @expects_strict = true
22
29
  self
23
30
  end
24
31
 
25
- def failure_message_when_negated
26
- @failure_message_when_negated || @failure_message
32
+ def expects_strict?
33
+ @expects_strict
34
+ end
35
+
36
+ def with_message(expected_message)
37
+ if expected_message
38
+ @expects_custom_validation_message = true
39
+ @expected_message = expected_message
40
+ end
41
+
42
+ self
43
+ end
44
+
45
+ def expects_custom_validation_message?
46
+ @expects_custom_validation_message
27
47
  end
28
48
 
29
49
  def matches?(subject)
@@ -31,66 +51,108 @@ module Shoulda
31
51
  false
32
52
  end
33
53
 
34
- private
54
+ def failure_message
55
+ overall_failure_message.dup.tap do |message|
56
+ if failure_reason.present?
57
+ message << "\n"
58
+ message << Shoulda::Matchers.word_wrap(
59
+ failure_reason,
60
+ indent: 2
61
+ )
62
+ end
63
+ end
64
+ end
35
65
 
36
- def allows_value_of(value, message = nil, &block)
37
- allow = allow_value_matcher(value, message)
38
- yield allow if block_given?
39
-
40
- if allow.matches?(@subject)
41
- @failure_message_when_negated = allow.failure_message_when_negated
42
- true
43
- else
44
- @failure_message = allow.failure_message
45
- false
66
+ def failure_message_when_negated
67
+ overall_failure_message_when_negated.dup.tap do |message|
68
+ if failure_reason_when_negated.present?
69
+ message << "\n"
70
+ message << Shoulda::Matchers.word_wrap(
71
+ failure_reason_when_negated,
72
+ indent: 2
73
+ )
74
+ end
46
75
  end
47
76
  end
48
77
 
78
+ protected
79
+
80
+ attr_reader :attribute, :context, :subject, :last_submatcher_run
81
+
82
+ def model
83
+ subject.class
84
+ end
85
+
86
+ def allows_value_of(value, message = nil, &block)
87
+ matcher = allow_value_matcher(value, message, &block)
88
+ run_allow_or_disallow_matcher(matcher)
89
+ end
90
+
49
91
  def disallows_value_of(value, message = nil, &block)
50
- disallow = disallow_value_matcher(value, message)
51
- yield disallow if block_given?
52
-
53
- if disallow.matches?(@subject)
54
- @failure_message_when_negated = disallow.failure_message_when_negated
55
- true
56
- else
57
- @failure_message = disallow.failure_message
58
- false
59
- end
92
+ matcher = disallow_value_matcher(value, message, &block)
93
+ run_allow_or_disallow_matcher(matcher)
60
94
  end
61
95
 
62
- def allow_value_matcher(value, message)
63
- matcher = AllowValueMatcher.new(value).for(@attribute).
64
- with_message(message)
96
+ def allow_value_matcher(value, message = nil, &block)
97
+ build_allow_or_disallow_value_matcher(
98
+ matcher_class: AllowValueMatcher,
99
+ value: value,
100
+ message: message,
101
+ &block
102
+ )
103
+ end
65
104
 
66
- if defined?(@context)
67
- matcher.on(@context)
68
- end
105
+ def disallow_value_matcher(value, message = nil, &block)
106
+ build_allow_or_disallow_value_matcher(
107
+ matcher_class: DisallowValueMatcher,
108
+ value: value,
109
+ message: message,
110
+ &block
111
+ )
112
+ end
69
113
 
70
- if strict?
71
- matcher.strict
72
- end
114
+ private
73
115
 
74
- matcher
116
+ def overall_failure_message
117
+ Shoulda::Matchers.word_wrap(
118
+ "#{model.name} did not properly #{description}."
119
+ )
75
120
  end
76
121
 
77
- def disallow_value_matcher(value, message)
78
- matcher = DisallowValueMatcher.new(value).for(@attribute).
79
- with_message(message)
122
+ def overall_failure_message_when_negated
123
+ Shoulda::Matchers.word_wrap(
124
+ "Expected #{model.name} not to #{description}, but it did."
125
+ )
126
+ end
80
127
 
81
- if defined?(@context)
82
- matcher.on(@context)
83
- end
128
+ def failure_reason
129
+ last_submatcher_run.try(:failure_message)
130
+ end
84
131
 
85
- if strict?
86
- matcher.strict
87
- end
132
+ def failure_reason_when_negated
133
+ last_submatcher_run.try(:failure_message_when_negated)
134
+ end
135
+
136
+ def build_allow_or_disallow_value_matcher(args)
137
+ matcher_class = args.fetch(:matcher_class)
138
+ value = args.fetch(:value)
139
+ message = args[:message]
140
+
141
+ matcher = matcher_class.new(value).
142
+ for(attribute).
143
+ with_message(message).
144
+ on(context).
145
+ strict(expects_strict?).
146
+ ignoring_interference_by_writer(ignore_interference_by_writer)
147
+
148
+ yield matcher if block_given?
88
149
 
89
150
  matcher
90
151
  end
91
152
 
92
- def strict?
93
- @strict
153
+ def run_allow_or_disallow_matcher(matcher)
154
+ @last_submatcher_run = matcher
155
+ matcher.matches?(subject)
94
156
  end
95
157
  end
96
158
  end
@@ -0,0 +1,60 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module ActiveModel
4
+ class ValidationMatcher
5
+ # @private
6
+ class BuildDescription
7
+ def self.call(matcher, main_description)
8
+ new(matcher, main_description).call
9
+ end
10
+
11
+ def initialize(matcher, main_description)
12
+ @matcher = matcher
13
+ @main_description = main_description
14
+ end
15
+
16
+ def call
17
+ if description_clauses_for_qualifiers.any?
18
+ main_description +
19
+ ', ' +
20
+ description_clauses_for_qualifiers.to_sentence
21
+ else
22
+ main_description
23
+ end
24
+ end
25
+
26
+ protected
27
+
28
+ attr_reader :matcher, :main_description
29
+
30
+ private
31
+
32
+ def description_clauses_for_qualifiers
33
+ description_clauses = []
34
+
35
+ if matcher.try(:expects_to_allow_blank?)
36
+ description_clauses << 'but only if it is not blank'
37
+ elsif matcher.try(:expects_to_allow_nil?)
38
+ description_clauses << 'but only if it is not nil'
39
+ end
40
+
41
+ if matcher.try(:expects_strict?)
42
+ description_clauses << 'raising a validation exception'
43
+
44
+ if matcher.try(:expects_custom_validation_message?)
45
+ description_clauses.last << ' with a custom message'
46
+ end
47
+
48
+ description_clauses.last << ' on failure'
49
+ elsif matcher.try(:expects_custom_validation_message?)
50
+ description_clauses <<
51
+ 'producing a custom validation error on failure'
52
+ end
53
+
54
+ description_clauses
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -5,99 +5,118 @@ module Shoulda
5
5
  class Validator
6
6
  include Helpers
7
7
 
8
- attr_writer :attribute, :context, :record
8
+ def initialize(record, attribute, options = {})
9
+ @record = record
10
+ @attribute = attribute
11
+ @context = options[:context]
12
+ @expects_strict = options[:expects_strict]
13
+ @expected_message = options[:expected_message]
9
14
 
10
- def initialize
11
- reset
15
+ @_validation_result = nil
16
+ @captured_validation_exception = false
17
+ @captured_range_error = false
12
18
  end
13
19
 
14
- def reset
15
- @messages = nil
20
+ def call
21
+ !messages_match? && !captured_range_error?
16
22
  end
17
23
 
18
- def strict=(strict)
19
- @strict = strict
24
+ def has_messages?
25
+ messages.any?
26
+ end
20
27
 
21
- if strict
22
- extend StrictValidator
23
- end
28
+ def captured_validation_exception?
29
+ @captured_validation_exception
24
30
  end
25
31
 
26
- def allow_description(allowed_values)
27
- "allow #{attribute} to be set to #{allowed_values}"
32
+ def type_of_message_matched?
33
+ expects_strict? == captured_validation_exception?
28
34
  end
29
35
 
30
- def expected_message_from(attribute_message)
31
- attribute_message
36
+ def all_formatted_validation_error_messages
37
+ format_validation_errors(all_validation_errors)
32
38
  end
33
39
 
34
- def messages
35
- @messages ||= collect_messages
40
+ def validation_exception_message
41
+ validation_result[:validation_exception_message]
36
42
  end
37
43
 
38
- def formatted_messages
39
- messages
44
+ protected
45
+
46
+ attr_reader :attribute, :context, :record
47
+
48
+ private
49
+
50
+ def expects_strict?
51
+ @expects_strict
40
52
  end
41
53
 
42
- def has_messages?
43
- messages.any?
54
+ def messages_match?
55
+ has_messages? &&
56
+ type_of_message_matched? &&
57
+ matched_messages.compact.any?
44
58
  end
45
59
 
46
- def messages_description
47
- if has_messages?
48
- " errors:\n#{pretty_error_messages(record)}"
60
+ def messages
61
+ if expects_strict?
62
+ [validation_exception_message]
49
63
  else
50
- ' no errors'
64
+ validation_error_messages
51
65
  end
52
66
  end
53
67
 
54
- def expected_messages_description(expected_message)
55
- if expected_message
56
- "errors to include #{expected_message.inspect}"
68
+ def matched_messages
69
+ if @expected_message
70
+ messages.grep(@expected_message)
57
71
  else
58
- 'errors'
72
+ messages
59
73
  end
60
74
  end
61
75
 
62
76
  def captured_range_error?
63
- !!captured_range_error
77
+ !!@captured_range_error
64
78
  end
65
79
 
66
- protected
67
-
68
- attr_reader :attribute, :context, :strict, :record,
69
- :captured_range_error
70
-
71
- def collect_messages
72
- validation_errors
80
+ def all_validation_errors
81
+ validation_result[:all_validation_errors]
73
82
  end
74
83
 
75
- private
76
-
77
- def strict?
78
- !!@strict
84
+ def validation_error_messages
85
+ validation_result[:validation_error_messages]
79
86
  end
80
87
 
81
- def collect_errors_or_exceptions
82
- collect_messages
88
+ def validation_result
89
+ @_validation_result ||= perform_validation
83
90
  end
84
91
 
85
- def validation_errors
92
+ def perform_validation
86
93
  if context
87
94
  record.valid?(context)
88
95
  else
89
96
  record.valid?
90
97
  end
91
98
 
92
- if record.errors.respond_to?(:[])
93
- record.errors[attribute]
94
- else
95
- record.errors.on(attribute)
96
- end
97
- end
98
-
99
- def human_attribute_name
100
- record.class.human_attribute_name(attribute)
99
+ all_validation_errors = record.errors.dup
100
+
101
+ validation_error_messages =
102
+ if record.errors.respond_to?(:[])
103
+ record.errors[attribute]
104
+ else
105
+ record.errors.on(attribute)
106
+ end
107
+
108
+ {
109
+ all_validation_errors: all_validation_errors,
110
+ validation_error_messages: validation_error_messages,
111
+ validation_exception_message: nil
112
+ }
113
+ rescue ::ActiveModel::StrictValidationFailed => exception
114
+ @captured_validation_exception = true
115
+ {
116
+ all_validation_errors: nil,
117
+ validation_error_messages: [],
118
+ validation_exception_message: exception.message
119
+ }
101
120
  end
102
121
  end
103
122
  end
@@ -63,8 +63,8 @@ module Shoulda
63
63
  end
64
64
 
65
65
  def matches?(subject)
66
- @model = subject
67
- enum_defined? && enum_values_match?
66
+ @record = subject
67
+ enum_defined? && enum_values_match? && column_type_is_integer?
68
68
  end
69
69
 
70
70
  def failure_message
@@ -84,15 +84,17 @@ module Shoulda
84
84
  desc << " with #{options[:expected_enum_values]}"
85
85
  end
86
86
 
87
+ desc << " and store the value in a column with an integer type"
88
+
87
89
  desc
88
90
  end
89
91
 
90
92
  protected
91
93
 
92
- attr_reader :model, :attribute_name, :options
94
+ attr_reader :record, :attribute_name, :options
93
95
 
94
96
  def expectation
95
- "#{model.class.name} to #{description}"
97
+ "#{model.name} to #{description}"
96
98
  end
97
99
 
98
100
  def expected_enum_values
@@ -100,7 +102,7 @@ module Shoulda
100
102
  end
101
103
 
102
104
  def actual_enum_values
103
- model.class.send(attribute_name.to_s.pluralize)
105
+ model.send(attribute_name.to_s.pluralize)
104
106
  end
105
107
 
106
108
  def enum_defined?
@@ -111,6 +113,18 @@ module Shoulda
111
113
  expected_enum_values.empty? || actual_enum_values == expected_enum_values
112
114
  end
113
115
 
116
+ def column_type_is_integer?
117
+ column.type == :integer
118
+ end
119
+
120
+ def column
121
+ model.columns_hash[attribute_name.to_s]
122
+ end
123
+
124
+ def model
125
+ record.class
126
+ end
127
+
114
128
  def hashify(value)
115
129
  if value.nil?
116
130
  return {}