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
@@ -32,14 +32,42 @@ describe Shoulda::Matchers::ActiveModel::ValidateAbsenceOfMatcher, type: :model
32
32
  expect(validating_absence_of(:attr, {}, type: type)).
33
33
  to validate_absence_of(:attr)
34
34
  end
35
+
36
+ it_supports(
37
+ 'ignoring_interference_by_writer',
38
+ tests: {
39
+ accept_if_qualified_but_changing_value_does_not_interfere: {
40
+ changing_values_with: :next_value
41
+ },
42
+ }
43
+ )
44
+
45
+ define_method(:validation_matcher_scenario_args) do |*args|
46
+ super(*args).deep_merge(column_type: type)
47
+ end
35
48
  end
36
49
  end
50
+
51
+ def validation_matcher_scenario_args
52
+ super.deep_merge(model_creator: :active_record)
53
+ end
37
54
  end
38
55
 
39
56
  context 'a model without an absence validation' do
40
- it 'rejects' do
41
- model = define_model(:example, attr: :string).new
42
- expect(model).not_to validate_absence_of(:attr)
57
+ it 'rejects with the correct failure message' do
58
+ record = define_model(:example, attr: :string).new
59
+
60
+ message = <<-MESSAGE
61
+ Example did not properly validate that :attr is empty/falsy.
62
+ After setting :attr to ‹"an arbitrary value"›, the matcher expected
63
+ the Example to be invalid, but it was valid instead.
64
+ MESSAGE
65
+
66
+ assertion = lambda do
67
+ expect(record).to validate_absence_of(:attr)
68
+ end
69
+
70
+ expect(&assertion).to fail_with_message(message)
43
71
  end
44
72
  end
45
73
 
@@ -51,17 +79,34 @@ describe Shoulda::Matchers::ActiveModel::ValidateAbsenceOfMatcher, type: :model
51
79
  it 'does not override the default message with a blank' do
52
80
  expect(active_model_validating_absence_of(:attr)).to validate_absence_of(:attr).with_message(nil)
53
81
  end
82
+
83
+ it_supports(
84
+ 'ignoring_interference_by_writer',
85
+ tests: {
86
+ accept_if_qualified_but_changing_value_does_not_interfere: {
87
+ changing_values_with: :upcase
88
+ },
89
+ }
90
+ )
91
+
92
+ def validation_matcher_scenario_args
93
+ super.deep_merge(model_creator: :active_model)
94
+ end
54
95
  end
55
96
 
56
97
  context 'an ActiveModel class without an absence validation' do
57
- it 'rejects' do
58
- expect(active_model_with(:attr)).not_to validate_absence_of(:attr)
59
- end
98
+ it 'rejects with the correct failure message' do
99
+ message = <<-MESSAGE
100
+ Example did not properly validate that :attr is empty/falsy.
101
+ After setting :attr to ‹"an arbitrary value"›, the matcher expected
102
+ the Example to be invalid, but it was valid instead.
103
+ MESSAGE
60
104
 
61
- it 'provides the correct failure message' do
62
- message = %{Expected errors to include "must be blank" when attr is set to "an arbitrary value",\ngot no errors}
105
+ assertion = lambda do
106
+ expect(active_model_with(:attr)).to validate_absence_of(:attr)
107
+ end
63
108
 
64
- expect { expect(active_model_with(:attr)).to validate_absence_of(:attr) }.to fail_with_message(message)
109
+ expect(&assertion).to fail_with_message(message)
65
110
  end
66
111
  end
67
112
 
@@ -69,6 +114,19 @@ describe Shoulda::Matchers::ActiveModel::ValidateAbsenceOfMatcher, type: :model
69
114
  it 'requires the attribute to not be set' do
70
115
  expect(having_many(:children, absence: true)).to validate_absence_of(:children)
71
116
  end
117
+
118
+ it_supports(
119
+ 'ignoring_interference_by_writer',
120
+ tests: {
121
+ accept_if_qualified_but_changing_value_does_not_interfere: {
122
+ changing_values_with: :next_value
123
+ },
124
+ }
125
+ )
126
+
127
+ def validation_matcher_scenario_args
128
+ super.deep_merge(model_creator: :"active_record/has_many")
129
+ end
72
130
  end
73
131
 
74
132
  context 'a has_many association without an absence validation' do
@@ -83,12 +141,36 @@ describe Shoulda::Matchers::ActiveModel::ValidateAbsenceOfMatcher, type: :model
83
141
  model = having_and_belonging_to_many(:children, absence: true)
84
142
  expect(model).to validate_absence_of(:children)
85
143
  end
144
+
145
+ it_supports(
146
+ 'ignoring_interference_by_writer',
147
+ tests: {
148
+ accept_if_qualified_but_changing_value_does_not_interfere: {
149
+ changing_values_with: :next_value
150
+ },
151
+ }
152
+ )
153
+
154
+ def validation_matcher_scenario_args
155
+ super.deep_merge(model_creator: :"active_record/habtm")
156
+ end
86
157
  end
87
158
 
88
159
  context 'a non-absent has_and_belongs_to_many association' do
89
- it 'rejects' do
160
+ it 'rejects with the correct failure message' do
90
161
  model = having_and_belonging_to_many(:children, absence: false)
91
- expect(model).not_to validate_absence_of(:children)
162
+
163
+ message = <<-MESSAGE
164
+ Parent did not properly validate that :children is empty/falsy.
165
+ After setting :children to ‹[#<Child id: nil>]›, the matcher expected
166
+ the Parent to be invalid, but it was valid instead.
167
+ MESSAGE
168
+
169
+ assertion = lambda do
170
+ expect(model).to validate_absence_of(:children)
171
+ end
172
+
173
+ expect(&assertion).to fail_with_message(message)
92
174
  end
93
175
  end
94
176
 
@@ -119,14 +201,28 @@ describe Shoulda::Matchers::ActiveModel::ValidateAbsenceOfMatcher, type: :model
119
201
  end
120
202
  end
121
203
 
122
- def validating_absence_of(attr, validation_options = {}, given_column_options = {})
204
+ def define_model_validating_absence_of(attr, validation_options = {}, given_column_options = {})
123
205
  default_column_options = { type: :string, options: {} }
124
206
  column_options = default_column_options.merge(given_column_options)
125
207
 
126
- define_model :example, attr => column_options do
127
- validates_absence_of attr, validation_options
128
- end.new
208
+ define_model :example, attr => column_options do |model|
209
+ model.validates_absence_of(attr, validation_options)
210
+
211
+ if block_given?
212
+ yield model
213
+ end
214
+ end
215
+ end
216
+
217
+ def validating_absence_of(attr, validation_options = {}, given_column_options = {})
218
+ model = define_model_validating_absence_of(
219
+ attr,
220
+ validation_options,
221
+ given_column_options
222
+ )
223
+ model.new
129
224
  end
225
+ alias_method :build_record_validating_absence_of, :validating_absence_of
130
226
 
131
227
  def active_model_with(attr, &block)
132
228
  define_active_model_class('Example', accessors: [attr], &block).new
@@ -162,5 +258,9 @@ describe Shoulda::Matchers::ActiveModel::ValidateAbsenceOfMatcher, type: :model
162
258
  end
163
259
  end.new
164
260
  end
261
+
262
+ def validation_matcher_scenario_args
263
+ super.deep_merge(matcher_name: :validate_absence_of)
264
+ end
165
265
  end
166
266
  end
@@ -9,6 +9,32 @@ describe Shoulda::Matchers::ActiveModel::ValidateAcceptanceOfMatcher, type: :mod
9
9
  it 'does not overwrite the default message with nil' do
10
10
  expect(record_validating_acceptance).to matcher.with_message(nil)
11
11
  end
12
+
13
+ it_supports(
14
+ 'ignoring_interference_by_writer',
15
+ tests: {
16
+ accept_if_qualified_but_changing_value_does_not_interfere: {
17
+ changing_values_with: :never_falsy,
18
+ },
19
+ reject_if_qualified_but_changing_value_interferes: {
20
+ model_name: 'Example',
21
+ attribute_name: :attr,
22
+ changing_values_with: :always_nil,
23
+ expected_message: <<-MESSAGE.strip
24
+ Example did not properly validate that :attr has been set to "1".
25
+ After setting :attr to ‹false› -- which was read back as ‹nil› -- the
26
+ matcher expected the Example to be invalid, but it was valid instead.
27
+
28
+ As indicated in the message above, :attr seems to be changing certain
29
+ values as they are set, and this could have something to do with why
30
+ this test is failing. If you've overridden the writer method for this
31
+ attribute, then you may need to change it to make this test pass, or
32
+ do something else entirely.
33
+ MESSAGE
34
+ },
35
+ },
36
+ model_creator: :active_model
37
+ )
12
38
  end
13
39
 
14
40
  context 'a model without an acceptance validation' do
@@ -33,8 +59,9 @@ describe Shoulda::Matchers::ActiveModel::ValidateAcceptanceOfMatcher, type: :mod
33
59
  validate_acceptance_of(:attr)
34
60
  end
35
61
 
36
- def model_validating_nothing(&block)
37
- define_active_model_class(:example, accessors: [:attr], &block)
62
+ def model_validating_nothing(options = {}, &block)
63
+ attribute_name = options.fetch(:attribute_name, :attr)
64
+ define_active_model_class(:example, accessors: [attribute_name], &block)
38
65
  end
39
66
 
40
67
  def record_validating_nothing
@@ -42,12 +69,23 @@ describe Shoulda::Matchers::ActiveModel::ValidateAcceptanceOfMatcher, type: :mod
42
69
  end
43
70
 
44
71
  def model_validating_acceptance(options = {})
45
- model_validating_nothing do
46
- validates_acceptance_of :attr, options
72
+ attribute_name = options.fetch(:attribute_name, :attr)
73
+
74
+ model_validating_nothing(attribute_name: attribute_name) do
75
+ validates_acceptance_of attribute_name, options
47
76
  end
48
77
  end
49
78
 
79
+ alias_method :define_model_validating_acceptance, :model_validating_acceptance
80
+
50
81
  def record_validating_acceptance(options = {})
51
82
  model_validating_acceptance(options).new
52
83
  end
84
+
85
+ alias_method :build_record_validating_acceptance,
86
+ :record_validating_acceptance
87
+
88
+ def validation_matcher_scenario_args
89
+ { matcher_name: :validate_acceptance_of }
90
+ end
53
91
  end
@@ -6,7 +6,7 @@ describe Shoulda::Matchers::ActiveModel::ValidateConfirmationOfMatcher, type: :m
6
6
  context '#description' do
7
7
  it 'states that the confirmation must match its base attribute' do
8
8
  builder = builder_for_record_validating_confirmation
9
- message = "require #{builder.confirmation_attribute} to match #{builder.attribute_to_confirm}"
9
+ message = "validate that :#{builder.confirmation_attribute} matches :#{builder.attribute_to_confirm}"
10
10
  matcher = described_class.new(builder.attribute_to_confirm)
11
11
  expect(matcher.description).to eq(message)
12
12
  end
@@ -27,13 +27,95 @@ describe Shoulda::Matchers::ActiveModel::ValidateConfirmationOfMatcher, type: :m
27
27
  with_message(nil)
28
28
  end
29
29
  end
30
+
31
+ it_supports(
32
+ 'ignoring_interference_by_writer',
33
+ tests: {
34
+ reject_if_qualified_but_changing_value_interferes: {
35
+ model_name: 'Example',
36
+ attribute_name: :password,
37
+ changing_values_with: :next_value,
38
+ expected_message: <<-MESSAGE.strip
39
+ Example did not properly validate that :password_confirmation matches
40
+ :password.
41
+ After setting :password_confirmation to ‹"same value"›, then setting
42
+ :password to ‹"same value"› -- which was read back as ‹"same valuf"›
43
+ -- the matcher expected the Example to be valid, but it was invalid
44
+ instead, producing these validation errors:
45
+
46
+ * password_confirmation: ["doesn't match Password"]
47
+
48
+ As indicated in the message above, :password seems to be changing
49
+ certain values as they are set, and this could have something to do
50
+ with why this test is failing. If you've overridden the writer method
51
+ for this attribute, then you may need to change it to make this test
52
+ pass, or do something else entirely.
53
+ MESSAGE
54
+ },
55
+ },
56
+ model_creator: :active_model
57
+ )
58
+ end
59
+
60
+ context 'when the model does not have a confirmation attribute' do
61
+ it 'raises an AttributeDoesNotExistError' do
62
+ model = define_model(:example)
63
+
64
+ assertion = lambda do
65
+ expect(model.new).to validate_confirmation_of(:attribute_to_confirm)
66
+ end
67
+
68
+ message = <<-MESSAGE.rstrip
69
+ The matcher attempted to set :attribute_to_confirm_confirmation on the
70
+ Example to "some value", but that attribute does not exist.
71
+ MESSAGE
72
+
73
+ expect(&assertion).to raise_error(
74
+ Shoulda::Matchers::ActiveModel::AllowValueMatcher::AttributeDoesNotExistError,
75
+ message
76
+ )
77
+ end
78
+ end
79
+
80
+ context 'when the model does not have the attribute under test' do
81
+ it 'raises an AttributeDoesNotExistError' do
82
+ model = define_model(:example, attribute_to_confirm_confirmation: :string)
83
+
84
+ assertion = lambda do
85
+ expect(model.new).to validate_confirmation_of(:attribute_to_confirm)
86
+ end
87
+
88
+ message = <<-MESSAGE.rstrip
89
+ The matcher attempted to set :attribute_to_confirm on the Example to
90
+ "different value", but that attribute does not exist.
91
+ MESSAGE
92
+
93
+ expect(&assertion).to raise_error(
94
+ Shoulda::Matchers::ActiveModel::AllowValueMatcher::AttributeDoesNotExistError,
95
+ message
96
+ )
97
+ end
30
98
  end
31
99
 
32
- context 'when the model does not have a confirmation validation' do
33
- it 'fails' do
34
- model = define_model(:example, attribute_to_confirm: :string)
35
- record = model.new
36
- expect(record).not_to validate_confirmation_of(:attribute_to_confirm)
100
+ context 'when the model has all attributes, but does not have the validation' do
101
+ it 'fails with an appropriate failure message' do
102
+ model = define_model(:example, attribute_to_confirm: :string) do
103
+ attr_accessor :attribute_to_confirm_confirmation
104
+ end
105
+
106
+ assertion = lambda do
107
+ expect(model.new).to validate_confirmation_of(:attribute_to_confirm)
108
+ end
109
+
110
+ message = <<-MESSAGE
111
+ Example did not properly validate that
112
+ :attribute_to_confirm_confirmation matches :attribute_to_confirm.
113
+ After setting :attribute_to_confirm_confirmation to ‹"some value"›,
114
+ then setting :attribute_to_confirm to ‹"different value"›, the matcher
115
+ expected the Example to be invalid, but it was valid instead.
116
+ MESSAGE
117
+
118
+ expect(&assertion).to fail_with_message(message)
37
119
  end
38
120
  end
39
121
 
@@ -60,4 +142,8 @@ describe Shoulda::Matchers::ActiveModel::ValidateConfirmationOfMatcher, type: :m
60
142
  to validate_confirmation_of(builder.attribute_to_confirm)
61
143
  end
62
144
  end
145
+
146
+ def validation_matcher_scenario_args
147
+ super.deep_merge(matcher_name: :validate_confirmation_of)
148
+ end
63
149
  end
@@ -16,6 +16,41 @@ describe Shoulda::Matchers::ActiveModel::ValidateExclusionOfMatcher, type: :mode
16
16
  expect(validating_exclusion(in: 2..5)).
17
17
  to validate_exclusion_of(:attr).in_range(2..5).with_message(nil)
18
18
  end
19
+
20
+ it_supports(
21
+ 'ignoring_interference_by_writer',
22
+ tests: {
23
+ reject_if_qualified_but_changing_value_interferes: {
24
+ model_name: 'Example',
25
+ attribute_name: :attr,
26
+ changing_values_with: :next_value,
27
+ expected_message: <<-MESSAGE.strip
28
+ Example did not properly validate that :attr lies outside the range ‹2›
29
+ to ‹5›.
30
+ After setting :attr to ‹1› -- which was read back as ‹2› -- the
31
+ matcher expected the Example to be valid, but it was invalid instead,
32
+ producing these validation errors:
33
+
34
+ * attr: ["is reserved"]
35
+
36
+ As indicated in the message above, :attr seems to be changing certain
37
+ values as they are set, and this could have something to do with why
38
+ this test is failing. If you've overridden the writer method for this
39
+ attribute, then you may need to change it to make this test pass, or
40
+ do something else entirely.
41
+ MESSAGE
42
+ },
43
+ },
44
+ model_creator: :active_model
45
+ ) do
46
+ def validation_matcher_scenario_args
47
+ super.deep_merge(validation_options: { in: 2..5 })
48
+ end
49
+
50
+ def configure_validation_matcher(matcher)
51
+ matcher.in_range(2..5)
52
+ end
53
+ end
19
54
  end
20
55
 
21
56
  context 'an attribute which must be excluded from a range with excluded end' do
@@ -57,6 +92,14 @@ describe Shoulda::Matchers::ActiveModel::ValidateExclusionOfMatcher, type: :mode
57
92
  expect(model).to validate_exclusion_of(:attr).in_range(2...5).
58
93
  with_message(/should be out of this range/)
59
94
  end
95
+
96
+ it 'has correct description' do
97
+ matcher = validate_exclusion_of(:attr).in_range(1..10)
98
+
99
+ expect(matcher.description).to eq(
100
+ 'validate that :attr lies outside the range ‹1› to ‹10›'
101
+ )
102
+ end
60
103
  end
61
104
 
62
105
  context 'an attribute which must be excluded from an array' do
@@ -75,21 +118,90 @@ describe Shoulda::Matchers::ActiveModel::ValidateExclusionOfMatcher, type: :mode
75
118
  not_to validate_exclusion_of(:attr).in_array(%w(cat dog))
76
119
  end
77
120
 
78
- it 'has correct description' do
79
- expect(validate_exclusion_of(:attr).in_array([true, 'dog']).description).
80
- to eq 'ensure exclusion of attr in [true, "dog"]'
121
+ context 'when there is one value' do
122
+ it 'has correct description' do
123
+ expect(validate_exclusion_of(:attr).in_array([true]).description).
124
+ to eq 'validate that :attr is not ‹true›'
125
+ end
81
126
  end
82
127
 
83
- def validating_exclusion(options)
84
- define_model(:example, attr: :string) do
85
- validates_exclusion_of :attr, options
86
- end.new
128
+ context 'when there are two values' do
129
+ it 'has correct description' do
130
+ matcher = validate_exclusion_of(:attr).in_array([true, 'dog'])
131
+
132
+ expect(matcher.description).to eq(
133
+ 'validate that :attr is neither ‹true› nor ‹"dog"›'
134
+ )
135
+ end
136
+ end
137
+
138
+ context 'when there are three or more values' do
139
+ it 'has correct description' do
140
+ matcher = validate_exclusion_of(:attr).in_array([true, 'dog', 'cat'])
141
+
142
+ expect(matcher.description).to eq(
143
+ 'validate that :attr is neither ‹true›, ‹"dog"›, nor ‹"cat"›'
144
+ )
145
+ end
146
+ end
147
+
148
+ it_supports(
149
+ 'ignoring_interference_by_writer',
150
+ tests: {
151
+ reject_if_qualified_but_changing_value_interferes: {
152
+ model_name: 'Example',
153
+ attribute_name: :attr,
154
+ changing_values_with: :next_value,
155
+ expected_message: <<-MESSAGE.strip
156
+ Example did not properly validate that :attr is neither ‹"one"› nor
157
+ ‹"two"›.
158
+ After setting :attr to ‹"one"› -- which was read back as ‹"onf"› --
159
+ the matcher expected the Example to be invalid, but it was valid
160
+ instead.
161
+
162
+ As indicated in the message above, :attr seems to be changing certain
163
+ values as they are set, and this could have something to do with why
164
+ this test is failing. If you've overridden the writer method for this
165
+ attribute, then you may need to change it to make this test pass, or
166
+ do something else entirely.
167
+ MESSAGE
168
+ },
169
+ },
170
+ model_creator: :active_model
171
+ ) do
172
+ def validation_matcher_scenario_args
173
+ super.deep_merge(validation_options: { in: ['one', 'two'] })
174
+ end
175
+
176
+ def configure_validation_matcher(matcher)
177
+ matcher.in_array(['one', 'two'])
178
+ end
179
+ end
180
+
181
+ def define_model_validating_exclusion(options)
182
+ options = options.dup
183
+ column_type = options.delete(:column_type) { :string }
184
+ super options.merge(column_type: column_type)
185
+ end
186
+ end
187
+
188
+ def define_model_validating_exclusion(options)
189
+ options = options.dup
190
+ attribute_name = options.delete(:attribute_name) { :attr }
191
+ column_type = options.delete(:column_type) { :integer }
192
+
193
+ define_model(:example, attribute_name => column_type) do |model|
194
+ model.validates_exclusion_of(attribute_name, options)
87
195
  end
88
196
  end
89
197
 
90
198
  def validating_exclusion(options)
91
- define_model(:example, attr: :integer) do
92
- validates_exclusion_of :attr, options
93
- end.new
199
+ define_model_validating_exclusion(options).new
200
+ end
201
+
202
+ alias_method :build_record_validating_exclusion, :validating_exclusion
203
+
204
+ def validation_matcher_scenario_args
205
+ super.deep_merge(matcher_name: :validate_exclusion_of)
94
206
  end
95
207
  end