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,30 @@
1
+ require 'shoulda/matchers/active_model/helpers'
2
+ require 'shoulda/matchers/active_model/validation_matcher'
3
+ require 'shoulda/matchers/active_model/validation_message_finder'
4
+ require 'shoulda/matchers/active_model/exception_message_finder'
5
+ require 'shoulda/matchers/active_model/allow_value_matcher'
6
+ require 'shoulda/matchers/active_model/disallow_value_matcher'
7
+ require 'shoulda/matchers/active_model/ensure_length_of_matcher'
8
+ require 'shoulda/matchers/active_model/ensure_inclusion_of_matcher'
9
+ require 'shoulda/matchers/active_model/ensure_exclusion_of_matcher'
10
+ require 'shoulda/matchers/active_model/validate_absence_of_matcher'
11
+ require 'shoulda/matchers/active_model/validate_presence_of_matcher'
12
+ require 'shoulda/matchers/active_model/validate_uniqueness_of_matcher'
13
+ require 'shoulda/matchers/active_model/validate_acceptance_of_matcher'
14
+ require 'shoulda/matchers/active_model/validate_confirmation_of_matcher'
15
+ require 'shoulda/matchers/active_model/validate_numericality_of_matcher'
16
+ require 'shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher'
17
+ require 'shoulda/matchers/active_model/numericality_matchers/comparison_matcher'
18
+ require 'shoulda/matchers/active_model/numericality_matchers/odd_number_matcher'
19
+ require 'shoulda/matchers/active_model/numericality_matchers/even_number_matcher'
20
+ require 'shoulda/matchers/active_model/numericality_matchers/only_integer_matcher'
21
+ require 'shoulda/matchers/active_model/allow_mass_assignment_of_matcher'
22
+ require 'shoulda/matchers/active_model/errors'
23
+ require 'shoulda/matchers/active_model/have_secure_password_matcher'
24
+
25
+ module Shoulda
26
+ module Matchers
27
+ module ActiveModel
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,167 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module ActiveModel
4
+ # The `allow_mass_assignment_of` matcher tests usage of Rails 3's
5
+ # `attr_accessible` and `attr_protected` macros, asserting that an
6
+ # attribute in your model is contained in either the whitelist or
7
+ # blacklist and thus can or cannot be set via mass assignment.
8
+ #
9
+ # class Post
10
+ # include ActiveModel::Model
11
+ #
12
+ # attr_accessible :title
13
+ # end
14
+ #
15
+ # class User
16
+ # include ActiveModel::Model
17
+ #
18
+ # attr_protected :encrypted_password
19
+ # end
20
+ #
21
+ # # RSpec
22
+ # describe Post do
23
+ # it { should allow_mass_assignment_of(:title) }
24
+ # end
25
+ #
26
+ # describe User do
27
+ # it { should_not allow_mass_assignment_of(:encrypted_password) }
28
+ # end
29
+ #
30
+ # # Test::Unit
31
+ # class PostTest < ActiveSupport::TestCase
32
+ # should allow_mass_assignment_of(:title)
33
+ # end
34
+ #
35
+ # class UserTest < ActiveSupport::TestCase
36
+ # should_not allow_mass_assignment_of(:encrypted_password)
37
+ # end
38
+ #
39
+ # #### Qualifiers
40
+ #
41
+ # ##### as
42
+ #
43
+ # Use `as` if your mass-assignment rules apply only under a certain role
44
+ # (Rails >= 3.1 only).
45
+ #
46
+ # class Post
47
+ # include ActiveModel::Model
48
+ #
49
+ # attr_accessible :title, as: :admin
50
+ # end
51
+ #
52
+ # # RSpec
53
+ # describe Post do
54
+ # it { should allow_mass_assignment_of(:title).as(:admin) }
55
+ # end
56
+ #
57
+ # # Test::Unit
58
+ # class PostTest < ActiveSupport::TestCase
59
+ # should allow_mass_assignment_of(:title).as(:admin)
60
+ # end
61
+ #
62
+ # @return [AllowMassAssignmentOfMatcher]
63
+ #
64
+ def allow_mass_assignment_of(value)
65
+ AllowMassAssignmentOfMatcher.new(value)
66
+ end
67
+
68
+ # @private
69
+ class AllowMassAssignmentOfMatcher
70
+ attr_reader :failure_message, :failure_message_when_negated
71
+
72
+ alias failure_message_for_should failure_message
73
+ alias failure_message_for_should_not failure_message_when_negated
74
+
75
+ def initialize(attribute)
76
+ @attribute = attribute.to_s
77
+ @options = {}
78
+ end
79
+
80
+ def as(role)
81
+ if active_model_less_than_3_1?
82
+ raise 'You can specify role only in Rails 3.1 or greater'
83
+ end
84
+ @options[:role] = role
85
+ self
86
+ end
87
+
88
+ def matches?(subject)
89
+ @subject = subject
90
+ if attr_mass_assignable?
91
+ if whitelisting?
92
+ @failure_message_when_negated = "#{@attribute} was made accessible"
93
+ else
94
+ if protected_attributes.empty?
95
+ @failure_message_when_negated = 'no attributes were protected'
96
+ else
97
+ @failure_message_when_negated = "#{class_name} is protecting " <<
98
+ "#{protected_attributes.to_a.to_sentence}, " <<
99
+ "but not #{@attribute}."
100
+ end
101
+ end
102
+ true
103
+ else
104
+ if whitelisting?
105
+ @failure_message = "Expected #{@attribute} to be accessible"
106
+ else
107
+ @failure_message = "Did not expect #{@attribute} to be protected"
108
+ end
109
+ false
110
+ end
111
+ end
112
+
113
+ def description
114
+ [base_description, role_description].compact.join(' ')
115
+ end
116
+
117
+ private
118
+
119
+ def base_description
120
+ "allow mass assignment of #{@attribute}"
121
+ end
122
+
123
+ def role_description
124
+ if role != :default
125
+ "as #{role}"
126
+ end
127
+ end
128
+
129
+ def role
130
+ @options[:role] || :default
131
+ end
132
+
133
+ def protected_attributes
134
+ @protected_attributes ||= (@subject.class.protected_attributes || [])
135
+ end
136
+
137
+ def accessible_attributes
138
+ @accessible_attributes ||= (@subject.class.accessible_attributes || [])
139
+ end
140
+
141
+ def whitelisting?
142
+ authorizer.kind_of?(::ActiveModel::MassAssignmentSecurity::WhiteList)
143
+ end
144
+
145
+ def attr_mass_assignable?
146
+ !authorizer.deny?(@attribute)
147
+ end
148
+
149
+ def authorizer
150
+ if active_model_less_than_3_1?
151
+ @subject.class.active_authorizer
152
+ else
153
+ @subject.class.active_authorizer[role]
154
+ end
155
+ end
156
+
157
+ def class_name
158
+ @subject.class.name
159
+ end
160
+
161
+ def active_model_less_than_3_1?
162
+ ::ActiveModel::VERSION::STRING.to_f < 3.1
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,314 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module ActiveModel
4
+ # The `allow_value` matcher is used to test that an attribute of a model
5
+ # can or cannot be set to a particular value or values. It is most
6
+ # commonly used in conjunction with the `validates_format_of` validation.
7
+ #
8
+ # #### should
9
+ #
10
+ # In the positive form, `allow_value` asserts that an attribute can be
11
+ # set to one or more values, succeeding if none of the values cause the
12
+ # record to be invalid:
13
+ #
14
+ # class UserProfile
15
+ # include ActiveModel::Model
16
+ #
17
+ # validates_format_of :website_url, with: URI.regexp
18
+ # end
19
+ #
20
+ # # RSpec
21
+ # describe UserProfile do
22
+ # it { should allow_value('http://foo.com', 'http://bar.com/baz').for(:website_url) }
23
+ # end
24
+ #
25
+ # # Test::Unit
26
+ # class UserProfileTest < ActiveSupport::TestCase
27
+ # should allow_value('http://foo.com', 'http://bar.com/baz').for(:website_url)
28
+ # end
29
+ #
30
+ # #### should_not
31
+ #
32
+ # In the negative form, `allow_value` asserts that an attribute cannot be
33
+ # set to one or more values, succeeding if the *first* value causes the
34
+ # record to be invalid.
35
+ #
36
+ # **This can be surprising** so in this case if you need to check that
37
+ # *all* of the values are invalid, use separate assertions:
38
+ #
39
+ # class UserProfile
40
+ # include ActiveModel::Model
41
+ #
42
+ # validates_format_of :website_url, with: URI.regexp
43
+ # end
44
+ #
45
+ # describe UserProfile do
46
+ # # One assertion: 'buz' and 'bar' will not be tested
47
+ # it { should_not allow_value('fiz', 'buz', 'bar').for(:website_url) }
48
+ #
49
+ # # Three assertions, all tested separately
50
+ # it { should_not allow_value('fiz').for(:website_url) }
51
+ # it { should_not allow_value('buz').for(:website_url) }
52
+ # it { should_not allow_value('bar').for(:website_url) }
53
+ # end
54
+ #
55
+ # #### Qualifiers
56
+ #
57
+ # ##### on
58
+ #
59
+ # Use `on` if your validation applies only under a certain context.
60
+ #
61
+ # class UserProfile
62
+ # include ActiveModel::Model
63
+ #
64
+ # validates_format_of :birthday_as_string,
65
+ # with: /^(\d+)-(\d+)-(\d+)$/,
66
+ # on: :create
67
+ # end
68
+ #
69
+ # # RSpec
70
+ # describe UserProfile do
71
+ # it do
72
+ # should allow_value('2013-01-01').
73
+ # for(:birthday_as_string).
74
+ # on(:create)
75
+ # end
76
+ # end
77
+ #
78
+ # # Test::Unit
79
+ # class UserProfileTest < ActiveSupport::TestCase
80
+ # should allow_value('2013-01-01').
81
+ # for(:birthday_as_string).
82
+ # on(:create)
83
+ # end
84
+ #
85
+ # ##### with_message
86
+ #
87
+ # Use `with_message` if you are using a custom validation message.
88
+ #
89
+ # class UserProfile
90
+ # include ActiveModel::Model
91
+ #
92
+ # validates_format_of :state,
93
+ # with: /^(open|closed)$/,
94
+ # message: 'State must be open or closed'
95
+ # end
96
+ #
97
+ # # RSpec
98
+ # describe UserProfile do
99
+ # it do
100
+ # should allow_value('open', 'closed').
101
+ # for(:state).
102
+ # with_message('State must be open or closed')
103
+ # end
104
+ # end
105
+ #
106
+ # # Test::Unit
107
+ # class UserProfileTest < ActiveSupport::TestCase
108
+ # should allow_value('open', 'closed').
109
+ # for(:state).
110
+ # with_message('State must be open or closed')
111
+ # end
112
+ #
113
+ # Use `with_message` with the `:against` option if the attribute the
114
+ # validation message is stored under is different from the attribute
115
+ # being validated.
116
+ #
117
+ # class UserProfile
118
+ # include ActiveModel::Model
119
+ #
120
+ # validate :sports_team_must_be_valid
121
+ #
122
+ # private
123
+ #
124
+ # def sports_team_must_be_valid
125
+ # if sports_team !~ /^(Broncos|Titans)$/i
126
+ # self.errors.add :chosen_sports_team, 'Must be either a Broncos fan or a Titans fan'
127
+ # end
128
+ # end
129
+ # end
130
+ #
131
+ # # RSpec
132
+ # describe UserProfile do
133
+ # it do
134
+ # should allow_value('Broncos', 'Titans').
135
+ # for(:sports_team).
136
+ # with_message('Must be either a Broncos or Titans fan',
137
+ # against: :chosen_sports_team
138
+ # )
139
+ # end
140
+ # end
141
+ #
142
+ # # Test::Unit
143
+ # class UserProfileTest < ActiveSupport::TestCase
144
+ # should allow_value('Broncos', 'Titans').
145
+ # for(:sports_team).
146
+ # with_message('Must be either a Broncos or Titans fan',
147
+ # against: :chosen_sports_team
148
+ # )
149
+ # end
150
+ #
151
+ # @return [AllowValueMatcher]
152
+ #
153
+ def allow_value(*values)
154
+ if values.empty?
155
+ raise ArgumentError, 'need at least one argument'
156
+ else
157
+ AllowValueMatcher.new(*values)
158
+ end
159
+ end
160
+
161
+ # @private
162
+ class AllowValueMatcher
163
+ include Helpers
164
+
165
+ attr_accessor :attribute_with_message
166
+ attr_accessor :options
167
+
168
+ def initialize(*values)
169
+ self.values_to_match = values
170
+ self.message_finder_factory = ValidationMessageFinder
171
+ self.options = {}
172
+ end
173
+
174
+ def for(attribute)
175
+ self.attribute_to_set = attribute
176
+ self.attribute_to_check_message_against = attribute
177
+ self
178
+ end
179
+
180
+ def on(context)
181
+ @context = context
182
+ self
183
+ end
184
+
185
+ def with_message(message, options={})
186
+ self.options[:expected_message] = message
187
+ if options.key?(:against)
188
+ self.attribute_to_check_message_against = options[:against]
189
+ end
190
+ self
191
+ end
192
+
193
+ def strict
194
+ self.message_finder_factory = ExceptionMessageFinder
195
+ self
196
+ end
197
+
198
+ def matches?(instance)
199
+ self.instance = instance
200
+
201
+ values_to_match.none? do |value|
202
+ self.value = value
203
+ instance.__send__("#{attribute_to_set}=", value)
204
+ errors_match?
205
+ end
206
+ end
207
+
208
+ def failure_message
209
+ "Did not expect #{expectation}, got error: #{matched_error}"
210
+ end
211
+ alias failure_message_for_should failure_message
212
+
213
+ def failure_message_when_negated
214
+ "Expected #{expectation}, got #{error_description}"
215
+ end
216
+ alias failure_message_for_should_not failure_message_when_negated
217
+
218
+ def description
219
+ message_finder.allow_description(allowed_values)
220
+ end
221
+
222
+ private
223
+
224
+ attr_accessor :values_to_match, :message_finder_factory,
225
+ :instance, :attribute_to_set, :attribute_to_check_message_against,
226
+ :context, :value, :matched_error
227
+
228
+ def errors_match?
229
+ has_messages? && errors_for_attribute_match?
230
+ end
231
+
232
+ def has_messages?
233
+ message_finder.has_messages?
234
+ end
235
+
236
+ def errors_for_attribute_match?
237
+ if expected_message
238
+ self.matched_error = errors_match_regexp? || errors_match_string?
239
+ else
240
+ errors_for_attribute.compact.any?
241
+ end
242
+ end
243
+
244
+ def errors_for_attribute
245
+ message_finder.messages
246
+ end
247
+
248
+ def errors_match_regexp?
249
+ if Regexp === expected_message
250
+ errors_for_attribute.detect { |e| e =~ expected_message }
251
+ end
252
+ end
253
+
254
+ def errors_match_string?
255
+ if errors_for_attribute.include?(expected_message)
256
+ expected_message
257
+ end
258
+ end
259
+
260
+ def expectation
261
+ includes_expected_message = expected_message ? "to include #{expected_message.inspect}" : ''
262
+ [error_source, includes_expected_message, "when #{attribute_to_set} is set to #{value.inspect}"].join(' ')
263
+ end
264
+
265
+ def error_source
266
+ message_finder.source_description
267
+ end
268
+
269
+ def error_description
270
+ message_finder.messages_description
271
+ end
272
+
273
+ def allowed_values
274
+ if values_to_match.length > 1
275
+ "any of [#{values_to_match.map(&:inspect).join(', ')}]"
276
+ else
277
+ values_to_match.first.inspect
278
+ end
279
+ end
280
+
281
+ def expected_message
282
+ if options.key?(:expected_message)
283
+ if Symbol === options[:expected_message]
284
+ default_expected_message
285
+ else
286
+ options[:expected_message]
287
+ end
288
+ end
289
+ end
290
+
291
+ def default_expected_message
292
+ message_finder.expected_message_from(default_attribute_message)
293
+ end
294
+
295
+ def default_attribute_message
296
+ default_error_message(
297
+ options[:expected_message],
298
+ model_name: model_name,
299
+ instance: instance,
300
+ attribute: attribute_to_set
301
+ )
302
+ end
303
+
304
+ def model_name
305
+ instance.class.to_s.underscore
306
+ end
307
+
308
+ def message_finder
309
+ message_finder_factory.new(instance, attribute_to_check_message_against, context)
310
+ end
311
+ end
312
+ end
313
+ end
314
+ end