mcmire-shoulda-matchers 2.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.
Files changed (164) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.travis.yml +32 -0
  4. data/.yardopts +7 -0
  5. data/Appraisals +45 -0
  6. data/CONTRIBUTING.md +41 -0
  7. data/Gemfile +31 -0
  8. data/Gemfile.lock +166 -0
  9. data/MIT-LICENSE +22 -0
  10. data/NEWS.md +299 -0
  11. data/README.md +163 -0
  12. data/Rakefile +116 -0
  13. data/doc_config/gh-pages/index.html.erb +9 -0
  14. data/doc_config/yard/setup.rb +22 -0
  15. data/doc_config/yard/templates/default/fulldoc/html/css/bootstrap.css +5967 -0
  16. data/doc_config/yard/templates/default/fulldoc/html/css/full_list.css +12 -0
  17. data/doc_config/yard/templates/default/fulldoc/html/css/global.css +45 -0
  18. data/doc_config/yard/templates/default/fulldoc/html/css/solarized.css +69 -0
  19. data/doc_config/yard/templates/default/fulldoc/html/css/style.css +283 -0
  20. data/doc_config/yard/templates/default/fulldoc/html/full_list.erb +32 -0
  21. data/doc_config/yard/templates/default/fulldoc/html/full_list_class.erb +1 -0
  22. data/doc_config/yard/templates/default/fulldoc/html/full_list_method.erb +8 -0
  23. data/doc_config/yard/templates/default/fulldoc/html/js/app.js +300 -0
  24. data/doc_config/yard/templates/default/fulldoc/html/js/full_list.js +1 -0
  25. data/doc_config/yard/templates/default/fulldoc/html/js/jquery.stickyheaders.js +289 -0
  26. data/doc_config/yard/templates/default/fulldoc/html/js/underscore.min.js +6 -0
  27. data/doc_config/yard/templates/default/fulldoc/html/setup.rb +8 -0
  28. data/doc_config/yard/templates/default/layout/html/breadcrumb.erb +14 -0
  29. data/doc_config/yard/templates/default/layout/html/fonts.erb +1 -0
  30. data/doc_config/yard/templates/default/layout/html/layout.erb +23 -0
  31. data/doc_config/yard/templates/default/layout/html/search.erb +13 -0
  32. data/doc_config/yard/templates/default/layout/html/setup.rb +8 -0
  33. data/doc_config/yard/templates/default/method_details/html/source.erb +10 -0
  34. data/doc_config/yard/templates/default/module/html/box_info.erb +31 -0
  35. data/features/rails_integration.feature +113 -0
  36. data/features/step_definitions/rails_steps.rb +162 -0
  37. data/features/support/env.rb +5 -0
  38. data/gemfiles/3.0.gemfile +24 -0
  39. data/gemfiles/3.0.gemfile.lock +150 -0
  40. data/gemfiles/3.1.gemfile +27 -0
  41. data/gemfiles/3.1.gemfile.lock +173 -0
  42. data/gemfiles/3.2.gemfile +27 -0
  43. data/gemfiles/3.2.gemfile.lock +171 -0
  44. data/gemfiles/4.0.0.gemfile +28 -0
  45. data/gemfiles/4.0.0.gemfile.lock +172 -0
  46. data/gemfiles/4.0.1.gemfile +28 -0
  47. data/gemfiles/4.0.1.gemfile.lock +172 -0
  48. data/lib/shoulda-matchers.rb +1 -0
  49. data/lib/shoulda/matchers.rb +11 -0
  50. data/lib/shoulda/matchers/action_controller.rb +17 -0
  51. data/lib/shoulda/matchers/action_controller/filter_param_matcher.rb +64 -0
  52. data/lib/shoulda/matchers/action_controller/redirect_to_matcher.rb +97 -0
  53. data/lib/shoulda/matchers/action_controller/render_template_matcher.rb +81 -0
  54. data/lib/shoulda/matchers/action_controller/render_with_layout_matcher.rb +117 -0
  55. data/lib/shoulda/matchers/action_controller/rescue_from_matcher.rb +114 -0
  56. data/lib/shoulda/matchers/action_controller/respond_with_matcher.rb +154 -0
  57. data/lib/shoulda/matchers/action_controller/route_matcher.rb +116 -0
  58. data/lib/shoulda/matchers/action_controller/route_params.rb +48 -0
  59. data/lib/shoulda/matchers/action_controller/set_session_matcher.rb +164 -0
  60. data/lib/shoulda/matchers/action_controller/set_the_flash_matcher.rb +296 -0
  61. data/lib/shoulda/matchers/active_model.rb +30 -0
  62. data/lib/shoulda/matchers/active_model/allow_mass_assignment_of_matcher.rb +167 -0
  63. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +314 -0
  64. data/lib/shoulda/matchers/active_model/disallow_value_matcher.rb +46 -0
  65. data/lib/shoulda/matchers/active_model/ensure_exclusion_of_matcher.rb +160 -0
  66. data/lib/shoulda/matchers/active_model/ensure_inclusion_of_matcher.rb +417 -0
  67. data/lib/shoulda/matchers/active_model/ensure_length_of_matcher.rb +337 -0
  68. data/lib/shoulda/matchers/active_model/errors.rb +10 -0
  69. data/lib/shoulda/matchers/active_model/exception_message_finder.rb +58 -0
  70. data/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb +92 -0
  71. data/lib/shoulda/matchers/active_model/helpers.rb +46 -0
  72. data/lib/shoulda/matchers/active_model/numericality_matchers.rb +9 -0
  73. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +75 -0
  74. data/lib/shoulda/matchers/active_model/numericality_matchers/even_number_matcher.rb +27 -0
  75. data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +41 -0
  76. data/lib/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher.rb +27 -0
  77. data/lib/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher.rb +26 -0
  78. data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +112 -0
  79. data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +77 -0
  80. data/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +121 -0
  81. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +380 -0
  82. data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +89 -0
  83. data/lib/shoulda/matchers/active_model/validate_uniqueness_of_matcher.rb +372 -0
  84. data/lib/shoulda/matchers/active_model/validation_matcher.rb +97 -0
  85. data/lib/shoulda/matchers/active_model/validation_message_finder.rb +69 -0
  86. data/lib/shoulda/matchers/active_record.rb +22 -0
  87. data/lib/shoulda/matchers/active_record/accept_nested_attributes_for_matcher.rb +204 -0
  88. data/lib/shoulda/matchers/active_record/association_matcher.rb +901 -0
  89. data/lib/shoulda/matchers/active_record/association_matchers.rb +9 -0
  90. data/lib/shoulda/matchers/active_record/association_matchers/counter_cache_matcher.rb +41 -0
  91. data/lib/shoulda/matchers/active_record/association_matchers/dependent_matcher.rb +41 -0
  92. data/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +81 -0
  93. data/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +65 -0
  94. data/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb +94 -0
  95. data/lib/shoulda/matchers/active_record/association_matchers/order_matcher.rb +41 -0
  96. data/lib/shoulda/matchers/active_record/association_matchers/source_matcher.rb +41 -0
  97. data/lib/shoulda/matchers/active_record/association_matchers/through_matcher.rb +63 -0
  98. data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +261 -0
  99. data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +149 -0
  100. data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +72 -0
  101. data/lib/shoulda/matchers/active_record/serialize_matcher.rb +181 -0
  102. data/lib/shoulda/matchers/assertion_error.rb +19 -0
  103. data/lib/shoulda/matchers/error.rb +6 -0
  104. data/lib/shoulda/matchers/integrations/rspec.rb +20 -0
  105. data/lib/shoulda/matchers/integrations/test_unit.rb +30 -0
  106. data/lib/shoulda/matchers/rails_shim.rb +50 -0
  107. data/lib/shoulda/matchers/version.rb +6 -0
  108. data/lib/shoulda/matchers/warn.rb +8 -0
  109. data/shoulda-matchers.gemspec +23 -0
  110. data/spec/shoulda/matchers/action_controller/filter_param_matcher_spec.rb +22 -0
  111. data/spec/shoulda/matchers/action_controller/redirect_to_matcher_spec.rb +42 -0
  112. data/spec/shoulda/matchers/action_controller/render_template_matcher_spec.rb +78 -0
  113. data/spec/shoulda/matchers/action_controller/render_with_layout_matcher_spec.rb +63 -0
  114. data/spec/shoulda/matchers/action_controller/rescue_from_matcher_spec.rb +63 -0
  115. data/spec/shoulda/matchers/action_controller/respond_with_matcher_spec.rb +31 -0
  116. data/spec/shoulda/matchers/action_controller/route_matcher_spec.rb +70 -0
  117. data/spec/shoulda/matchers/action_controller/route_params_spec.rb +30 -0
  118. data/spec/shoulda/matchers/action_controller/set_session_matcher_spec.rb +51 -0
  119. data/spec/shoulda/matchers/action_controller/set_the_flash_matcher_spec.rb +153 -0
  120. data/spec/shoulda/matchers/active_model/allow_mass_assignment_of_matcher_spec.rb +111 -0
  121. data/spec/shoulda/matchers/active_model/allow_value_matcher_spec.rb +170 -0
  122. data/spec/shoulda/matchers/active_model/disallow_value_matcher_spec.rb +81 -0
  123. data/spec/shoulda/matchers/active_model/ensure_exclusion_of_matcher_spec.rb +95 -0
  124. data/spec/shoulda/matchers/active_model/ensure_inclusion_of_matcher_spec.rb +320 -0
  125. data/spec/shoulda/matchers/active_model/ensure_length_of_matcher_spec.rb +166 -0
  126. data/spec/shoulda/matchers/active_model/exception_message_finder_spec.rb +111 -0
  127. data/spec/shoulda/matchers/active_model/have_secure_password_matcher_spec.rb +20 -0
  128. data/spec/shoulda/matchers/active_model/helpers_spec.rb +158 -0
  129. data/spec/shoulda/matchers/active_model/numericality_matchers/comparison_matcher_spec.rb +169 -0
  130. data/spec/shoulda/matchers/active_model/numericality_matchers/even_number_matcher_spec.rb +59 -0
  131. data/spec/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher_spec.rb +59 -0
  132. data/spec/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher_spec.rb +57 -0
  133. data/spec/shoulda/matchers/active_model/validate_absence_of_matcher_spec.rb +139 -0
  134. data/spec/shoulda/matchers/active_model/validate_acceptance_of_matcher_spec.rb +41 -0
  135. data/spec/shoulda/matchers/active_model/validate_confirmation_of_matcher_spec.rb +47 -0
  136. data/spec/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +331 -0
  137. data/spec/shoulda/matchers/active_model/validate_presence_of_matcher_spec.rb +180 -0
  138. data/spec/shoulda/matchers/active_model/validate_uniqueness_of_matcher_spec.rb +398 -0
  139. data/spec/shoulda/matchers/active_model/validation_message_finder_spec.rb +127 -0
  140. data/spec/shoulda/matchers/active_record/accept_nested_attributes_for_matcher_spec.rb +107 -0
  141. data/spec/shoulda/matchers/active_record/association_matcher_spec.rb +860 -0
  142. data/spec/shoulda/matchers/active_record/association_matchers/model_reflection_spec.rb +247 -0
  143. data/spec/shoulda/matchers/active_record/have_db_column_matcher_spec.rb +111 -0
  144. data/spec/shoulda/matchers/active_record/have_db_index_matcher_spec.rb +78 -0
  145. data/spec/shoulda/matchers/active_record/have_readonly_attributes_matcher_spec.rb +41 -0
  146. data/spec/shoulda/matchers/active_record/serialize_matcher_spec.rb +86 -0
  147. data/spec/spec_helper.rb +26 -0
  148. data/spec/support/active_model_versions.rb +13 -0
  149. data/spec/support/active_resource_builder.rb +29 -0
  150. data/spec/support/activemodel_helpers.rb +19 -0
  151. data/spec/support/capture_helpers.rb +19 -0
  152. data/spec/support/class_builder.rb +42 -0
  153. data/spec/support/controller_builder.rb +74 -0
  154. data/spec/support/fail_with_message_including_matcher.rb +33 -0
  155. data/spec/support/fail_with_message_matcher.rb +32 -0
  156. data/spec/support/i18n_faker.rb +10 -0
  157. data/spec/support/mailer_builder.rb +10 -0
  158. data/spec/support/model_builder.rb +81 -0
  159. data/spec/support/rails_versions.rb +18 -0
  160. data/spec/support/shared_examples/numerical_submatcher.rb +19 -0
  161. data/spec/support/shared_examples/numerical_type_submatcher.rb +17 -0
  162. data/spec/support/test_application.rb +120 -0
  163. data/yard.watchr +5 -0
  164. metadata +281 -0
@@ -0,0 +1,46 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module ActiveModel
4
+ # @private
5
+ class DisallowValueMatcher
6
+ def initialize(value)
7
+ @allow_matcher = AllowValueMatcher.new(value)
8
+ end
9
+
10
+ def matches?(subject)
11
+ !@allow_matcher.matches?(subject)
12
+ end
13
+
14
+ def for(attribute)
15
+ @allow_matcher.for(attribute)
16
+ self
17
+ end
18
+
19
+ def on(context)
20
+ @allow_matcher.on(context)
21
+ self
22
+ end
23
+
24
+ def with_message(message, options={})
25
+ @allow_matcher.with_message(message, options)
26
+ self
27
+ end
28
+
29
+ def failure_message
30
+ @allow_matcher.failure_message_when_negated
31
+ end
32
+ alias failure_message_for_should failure_message
33
+
34
+ def failure_message_when_negated
35
+ @allow_matcher.failure_message
36
+ end
37
+ alias failure_message_for_should_not failure_message_when_negated
38
+
39
+ def strict
40
+ @allow_matcher.strict
41
+ self
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,160 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module ActiveModel
4
+ # The `ensure_exclusion_of` matcher tests usage of the
5
+ # `validates_exclusion_of` validation, asserting that an attribute cannot
6
+ # take a blacklist of values, and inversely, can take values outside of
7
+ # this list.
8
+ #
9
+ # #### Qualifiers
10
+ #
11
+ # `in_array` or `in_range` are used to test usage of the `:in` option,
12
+ # and so one must be used.
13
+ #
14
+ # ##### in_array
15
+ #
16
+ # Use `in_array` if your blacklist is an array of values.
17
+ #
18
+ # class Game
19
+ # include ActiveModel::Model
20
+ #
21
+ # validates_exclusion_of :supported_os, in: ['Mac', 'Linux']
22
+ # end
23
+ #
24
+ # # RSpec
25
+ # describe Game do
26
+ # it { should ensure_exclusion_of(:supported_os).in_array(['Mac', 'Linux']) }
27
+ # end
28
+ #
29
+ # # Test::Unit
30
+ # class GameTest < ActiveSupport::TestCase
31
+ # should ensure_exclusion_of(:supported_os).in_array(['Mac', 'Linux'])
32
+ # end
33
+ #
34
+ # ##### in_range
35
+ #
36
+ # Use `in_range` if your blacklist is a range of values.
37
+ #
38
+ # class Game
39
+ # include ActiveModel::Model
40
+ #
41
+ # validates_exclusion_of :supported_os, in: ['Mac', 'Linux']
42
+ # end
43
+ #
44
+ # # RSpec
45
+ # describe Game do
46
+ # it { should ensure_exclusion_of(:floors_with_enemies).in_range(5..8) }
47
+ # end
48
+ #
49
+ # # Test::Unit
50
+ # class GameTest < ActiveSupport::TestCase
51
+ # should ensure_exclusion_of(:floors_with_enemies).in_range(5..8)
52
+ # end
53
+ #
54
+ # ##### with_message
55
+ #
56
+ # Use `with_message` if you are using a custom validation message.
57
+ #
58
+ # class Game
59
+ # validates_exclusion_of :weapon,
60
+ # in: ['pistol', 'paintball gun', 'stick'],
61
+ # message: 'You chose a puny weapon'
62
+ # end
63
+ #
64
+ # # RSpec
65
+ # describe Game do
66
+ # it do
67
+ # should ensure_exclusion_of(:weapon).
68
+ # in_array(['pistol', 'paintball gun', 'stick']).
69
+ # with_message('You chose a puny weapon')
70
+ # end
71
+ # end
72
+ #
73
+ # # Test::Unit
74
+ # class GameTest < ActiveSupport::TestCase
75
+ # should ensure_exclusion_of(:weapon).
76
+ # in_array(['pistol', 'paintball gun', 'stick']).
77
+ # with_message('You chose a puny weapon')
78
+ # end
79
+ #
80
+ # @return [EnsureExclusionOfMatcher]
81
+ #
82
+ def ensure_exclusion_of(attr)
83
+ EnsureExclusionOfMatcher.new(attr)
84
+ end
85
+
86
+ # @private
87
+ class EnsureExclusionOfMatcher < ValidationMatcher
88
+ def in_array(array)
89
+ @array = array
90
+ self
91
+ end
92
+
93
+ def in_range(range)
94
+ @range = range
95
+ @minimum = range.first
96
+ @maximum = range.max
97
+ self
98
+ end
99
+
100
+ def with_message(message)
101
+ @expected_message = message if message
102
+ self
103
+ end
104
+
105
+ def description
106
+ "ensure exclusion of #{@attribute} in #{inspect_message}"
107
+ end
108
+
109
+ def matches?(subject)
110
+ super(subject)
111
+
112
+ if @range
113
+ allows_lower_value &&
114
+ disallows_minimum_value &&
115
+ allows_higher_value &&
116
+ disallows_maximum_value
117
+ elsif @array
118
+ disallows_all_values_in_array?
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ def disallows_all_values_in_array?
125
+ @array.all? do |value|
126
+ disallows_value_of(value, expected_message)
127
+ end
128
+ end
129
+
130
+ def allows_lower_value
131
+ @minimum == 0 || allows_value_of(@minimum - 1, expected_message)
132
+ end
133
+
134
+ def allows_higher_value
135
+ allows_value_of(@maximum + 1, expected_message)
136
+ end
137
+
138
+ def disallows_minimum_value
139
+ disallows_value_of(@minimum, expected_message)
140
+ end
141
+
142
+ def disallows_maximum_value
143
+ disallows_value_of(@maximum, expected_message)
144
+ end
145
+
146
+ def expected_message
147
+ @expected_message || :exclusion
148
+ end
149
+
150
+ def inspect_message
151
+ if @range
152
+ @range.inspect
153
+ else
154
+ @array.inspect
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,417 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module ActiveModel
4
+ # The `ensure_inclusion_of` matcher tests usage of the
5
+ # `validates_inclusion_of` validation, asserting that an attribute can
6
+ # take a whitelist of values and cannot take values outside of this list.
7
+ #
8
+ # #### Qualifiers
9
+ #
10
+ # `in_array` or `in_range` are used to test usage of the `:in` option,
11
+ # and so one must be used.
12
+ #
13
+ # ##### in_array
14
+ #
15
+ # Use `in_array` if your whitelist is an array of values.
16
+ #
17
+ # class Issue
18
+ # include ActiveModel::Model
19
+ #
20
+ # validates_inclusion_of :state, in: %w(open resolved unresolved)
21
+ # end
22
+ #
23
+ # # RSpec
24
+ # describe Issue do
25
+ # it { should ensure_inclusion_of(:state).in_array(%w(open resolved unresolved)) }
26
+ # end
27
+ #
28
+ # # Test::Unit
29
+ # class IssueTest < ActiveSupport::TestCase
30
+ # should ensure_inclusion_of(:state).in_array(%w(open resolved unresolved))
31
+ # end
32
+ #
33
+ # ##### in_range
34
+ #
35
+ # Use `in_range` if your whitelist is a range of values.
36
+ #
37
+ # class Issue
38
+ # include ActiveModel::Model
39
+ #
40
+ # validates_inclusion_of :priority, in: 1..5
41
+ # end
42
+ #
43
+ # # RSpec
44
+ # describe Issue do
45
+ # it { should ensure_inclusion_of(:state).in_range(1..5) }
46
+ # end
47
+ #
48
+ # # Test::Unit
49
+ # class IssueTest < ActiveSupport::TestCase
50
+ # should ensure_inclusion_of(:state).in_range(1..5)
51
+ # end
52
+ #
53
+ # ##### with_message
54
+ #
55
+ # Use `with_message` if you are using a custom validation message.
56
+ #
57
+ # class Issue
58
+ # include ActiveModel::Model
59
+ #
60
+ # validates_inclusion_of :severity,
61
+ # in: %w(low medium high),
62
+ # message: 'Severity must be low, medium, or high'
63
+ # end
64
+ #
65
+ # # RSpec
66
+ # describe Issue do
67
+ # it do
68
+ # should ensure_inclusion_of(:severity).
69
+ # in_array(%w(low medium high)).
70
+ # with_message('Severity must be low, medium, or high')
71
+ # end
72
+ # end
73
+ #
74
+ # # Test::Unit
75
+ # class IssueTest < ActiveSupport::TestCase
76
+ # should ensure_inclusion_of(:severity).
77
+ # in_array(%w(low medium high)).
78
+ # with_message('Severity must be low, medium, or high')
79
+ # end
80
+ #
81
+ # ##### with_low_message
82
+ #
83
+ # Use `with_low_message` if you have a custom validation message for when
84
+ # a given value is too low.
85
+ #
86
+ # class Person
87
+ # include ActiveModel::Model
88
+ #
89
+ # validate :age_must_be_valid
90
+ #
91
+ # private
92
+ #
93
+ # def age_must_be_valid
94
+ # if age < 65
95
+ # self.errors.add :age, 'You do not receive any benefits'
96
+ # end
97
+ # end
98
+ # end
99
+ #
100
+ # # RSpec
101
+ # describe Person do
102
+ # it do
103
+ # should ensure_inclusion_of(:age).
104
+ # in_range(0..65).
105
+ # with_low_message('You do not receive any benefits')
106
+ # end
107
+ # end
108
+ #
109
+ # # Test::Unit
110
+ # class PersonTest < ActiveSupport::TestCase
111
+ # should ensure_inclusion_of(:age).
112
+ # in_range(0..65).
113
+ # with_low_message('You do not receive any benefits')
114
+ # end
115
+ #
116
+ # ##### with_high_message
117
+ #
118
+ # Use `with_high_message` if you have a custom validation message for
119
+ # when a given value is too high.
120
+ #
121
+ # class Person
122
+ # include ActiveModel::Model
123
+ #
124
+ # validate :age_must_be_valid
125
+ #
126
+ # private
127
+ #
128
+ # def age_must_be_valid
129
+ # if age > 21
130
+ # self.errors.add :age, "You're too old for this stuff"
131
+ # end
132
+ # end
133
+ # end
134
+ #
135
+ # # RSpec
136
+ # describe Person do
137
+ # it do
138
+ # should ensure_inclusion_of(:age).
139
+ # in_range(0..21).
140
+ # with_high_message("You're too old for this stuff")
141
+ # end
142
+ # end
143
+ #
144
+ # # Test::Unit
145
+ # class PersonTest < ActiveSupport::TestCase
146
+ # should ensure_inclusion_of(:age).
147
+ # in_range(0..21).
148
+ # with_high_message("You're too old for this stuff")
149
+ # end
150
+ #
151
+ # ##### allow_nil
152
+ #
153
+ # Use `allow_nil` to assert that the attribute allows nil.
154
+ #
155
+ # class Issue
156
+ # include ActiveModel::Model
157
+ #
158
+ # validates_presence_of :state
159
+ # validates_inclusion_of :state,
160
+ # in: %w(open resolved unresolved),
161
+ # allow_nil: true
162
+ # end
163
+ #
164
+ # # RSpec
165
+ # describe Issue do
166
+ # it do
167
+ # should ensure_inclusion_of(:state).
168
+ # in_array(%w(open resolved unresolved)).
169
+ # allow_nil
170
+ # end
171
+ # end
172
+ #
173
+ # # Test::Unit
174
+ # class IssueTest < ActiveSupport::TestCase
175
+ # should ensure_inclusion_of(:state).
176
+ # in_array(%w(open resolved unresolved)).
177
+ # allow_nil
178
+ # end
179
+ #
180
+ # ##### allow_blank
181
+ #
182
+ # Use `allow_blank` to assert that the attribute allows blank.
183
+ #
184
+ # class Issue
185
+ # include ActiveModel::Model
186
+ #
187
+ # validates_presence_of :state
188
+ # validates_inclusion_of :state,
189
+ # in: %w(open resolved unresolved),
190
+ # allow_blank: true
191
+ # end
192
+ #
193
+ # # RSpec
194
+ # describe Issue do
195
+ # it do
196
+ # should ensure_inclusion_of(:state).
197
+ # in_array(%w(open resolved unresolved)).
198
+ # allow_blank
199
+ # end
200
+ # end
201
+ #
202
+ # # Test::Unit
203
+ # class IssueTest < ActiveSupport::TestCase
204
+ # should ensure_inclusion_of(:state).
205
+ # in_array(%w(open resolved unresolved)).
206
+ # allow_blank
207
+ # end
208
+ #
209
+ # @return [EnsureInclusionOfMatcher]
210
+ #
211
+ def ensure_inclusion_of(attr)
212
+ EnsureInclusionOfMatcher.new(attr)
213
+ end
214
+
215
+ # @private
216
+ class EnsureInclusionOfMatcher < ValidationMatcher
217
+ ARBITRARY_OUTSIDE_STRING = 'shouldamatchersteststring'
218
+ ARBITRARY_OUTSIDE_FIXNUM = 123456789
219
+ ARBITRARY_OUTSIDE_DECIMAL = 0.123456789
220
+ BOOLEAN_ALLOWS_BOOLEAN_MESSAGE = <<EOT
221
+ You are using `ensure_inclusion_of` to assert that a boolean column allows
222
+ boolean values and disallows non-boolean ones. Assuming you are using
223
+ `validates_format_of` in your model, be aware that it is not possible to fully
224
+ test this, and in fact the validation is superfluous, as boolean columns will
225
+ automatically convert non-boolean values to boolean ones. Hence, you should
226
+ consider removing this test and the corresponding validation.
227
+ EOT
228
+ BOOLEAN_ALLOWS_NIL_MESSAGE = <<EOT
229
+ You are using `ensure_inclusion_of` to assert that a boolean column allows nil.
230
+ Be aware that it is not possible to fully test this, as anything other than
231
+ true, false or nil will be converted to false. Hence, you should consider
232
+ removing this test and the corresponding validation.
233
+ EOT
234
+ BOOLEAN_ALLOWS_NIL_WITH_NOT_NULL_MESSAGE = <<EOT
235
+ You have specified that your model's #{@attribute} should ensure inclusion of nil.
236
+ However, #{@attribute} is a boolean column which does not allow null values.
237
+ Hence, this test will fail and there is no way to make it pass.
238
+ EOT
239
+
240
+ def initialize(attribute)
241
+ super(attribute)
242
+ @options = {}
243
+ end
244
+
245
+ def in_array(array)
246
+ @array = array
247
+ self
248
+ end
249
+
250
+ def in_range(range)
251
+ @range = range
252
+ @minimum = range.first
253
+ @maximum = range.max
254
+ self
255
+ end
256
+
257
+ def allow_blank(allow_blank = true)
258
+ @options[:allow_blank] = allow_blank
259
+ self
260
+ end
261
+
262
+ def allow_nil(allow_nil = true)
263
+ @options[:allow_nil] = allow_nil
264
+ self
265
+ end
266
+
267
+ def with_message(message)
268
+ if message
269
+ @low_message = message
270
+ @high_message = message
271
+ end
272
+ self
273
+ end
274
+
275
+ def with_low_message(message)
276
+ @low_message = message if message
277
+ self
278
+ end
279
+
280
+ def with_high_message(message)
281
+ @high_message = message if message
282
+ self
283
+ end
284
+
285
+ def description
286
+ "ensure inclusion of #{@attribute} in #{inspect_message}"
287
+ end
288
+
289
+ def matches?(subject)
290
+ super(subject)
291
+
292
+ if @range
293
+ @low_message ||= :inclusion
294
+ @high_message ||= :inclusion
295
+
296
+ disallows_lower_value &&
297
+ allows_minimum_value &&
298
+ disallows_higher_value &&
299
+ allows_maximum_value
300
+ elsif @array
301
+ if allows_all_values_in_array? && allows_blank_value? && allows_nil_value? && disallows_value_outside_of_array?
302
+ true
303
+ else
304
+ @failure_message = "#{@array} doesn't match array in validation"
305
+ false
306
+ end
307
+ end
308
+ end
309
+
310
+ private
311
+
312
+ def allows_blank_value?
313
+ if @options.key?(:allow_blank)
314
+ blank_values = ['', ' ', "\n", "\r", "\t", "\f"]
315
+ @options[:allow_blank] == blank_values.all? { |value| allows_value_of(value) }
316
+ else
317
+ true
318
+ end
319
+ end
320
+
321
+ def allows_nil_value?
322
+ if @options.key?(:allow_nil)
323
+ @options[:allow_nil] == allows_value_of(nil)
324
+ else
325
+ true
326
+ end
327
+ end
328
+
329
+ def inspect_message
330
+ @range.nil? ? @array.inspect : @range.inspect
331
+ end
332
+
333
+ def allows_all_values_in_array?
334
+ @array.all? do |value|
335
+ allows_value_of(value, @low_message)
336
+ end
337
+ end
338
+
339
+ def disallows_lower_value
340
+ @minimum == 0 || disallows_value_of(@minimum - 1, @low_message)
341
+ end
342
+
343
+ def disallows_higher_value
344
+ disallows_value_of(@maximum + 1, @high_message)
345
+ end
346
+
347
+ def allows_minimum_value
348
+ allows_value_of(@minimum, @low_message)
349
+ end
350
+
351
+ def allows_maximum_value
352
+ allows_value_of(@maximum, @high_message)
353
+ end
354
+
355
+ def disallows_value_outside_of_array?
356
+ if attribute_column.type == :boolean
357
+ case @array
358
+ when [true, false]
359
+ Shoulda::Matchers.warn BOOLEAN_ALLOWS_BOOLEAN_MESSAGE
360
+ return true
361
+ when [nil]
362
+ if attribute_column.null
363
+ Shoulda::Matchers.warn BOOLEAN_ALLOWS_NIL_MESSAGE
364
+ return true
365
+ else
366
+ raise NonNullableBooleanError, BOOLEAN_ALLOWS_NIL_WITH_NOT_NULL_MESSAGE
367
+ end
368
+ end
369
+ end
370
+
371
+ !allows_value_of(*values_outside_of_array)
372
+ end
373
+
374
+ def values_outside_of_array
375
+ if !(@array & outside_values).empty?
376
+ raise CouldNotDetermineValueOutsideOfArray
377
+ else
378
+ outside_values
379
+ end
380
+ end
381
+
382
+ def outside_values
383
+ case attribute_column.type
384
+ when :boolean
385
+ boolean_outside_values
386
+ when :integer, :float
387
+ [ARBITRARY_OUTSIDE_FIXNUM]
388
+ when :decimal
389
+ [ARBITRARY_OUTSIDE_DECIMAL]
390
+ else
391
+ [ARBITRARY_OUTSIDE_STRING]
392
+ end
393
+ end
394
+
395
+ def boolean_outside_values
396
+ values = []
397
+
398
+ values << case @array
399
+ when [true] then false
400
+ when [false] then true
401
+ else raise CouldNotDetermineValueOutsideOfArray
402
+ end
403
+
404
+ if attribute_column.null
405
+ values << nil
406
+ end
407
+
408
+ values
409
+ end
410
+
411
+ def attribute_column
412
+ @subject.class.columns_hash[@attribute.to_s]
413
+ end
414
+ end
415
+ end
416
+ end
417
+ end