shoulda-matchers 3.0.1 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
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 {}