forked from thoughtbot/shoulda-matchers
-
Notifications
You must be signed in to change notification settings - Fork 0
/
allow_value_matcher.rb
438 lines (392 loc) · 13 KB
/
allow_value_matcher.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
module Shoulda
module Matchers
module ActiveModel
# The `allow_value` matcher is used to test that an attribute of a model
# can or cannot be set to a particular value or values. It is most
# commonly used in conjunction with the `validates_format_of` validation.
#
# #### should
#
# In the positive form, `allow_value` asserts that an attribute can be
# set to one or more values, succeeding if none of the values cause the
# record to be invalid:
#
# class UserProfile
# include ActiveModel::Model
# attr_accessor :website_url
#
# validates_format_of :website_url, with: URI.regexp
# end
#
# # RSpec
# describe UserProfile do
# it do
# should allow_value('http://foo.com', 'http://bar.com/baz').
# for(:website_url)
# end
# end
#
# # Test::Unit
# class UserProfileTest < ActiveSupport::TestCase
# should allow_value('http://foo.com', 'http://bar.com/baz').
# for(:website_url)
# end
#
# #### should_not
#
# In the negative form, `allow_value` asserts that an attribute cannot be
# set to one or more values, succeeding if the *first* value causes the
# record to be invalid.
#
# **This can be surprising** so in this case if you need to check that
# *all* of the values are invalid, use separate assertions:
#
# class UserProfile
# include ActiveModel::Model
# attr_accessor :website_url
#
# validates_format_of :website_url, with: URI.regexp
# end
#
# describe UserProfile do
# # One assertion: 'buz' and 'bar' will not be tested
# it { should_not allow_value('fiz', 'buz', 'bar').for(:website_url) }
#
# # Three assertions, all tested separately
# it { should_not allow_value('fiz').for(:website_url) }
# it { should_not allow_value('buz').for(:website_url) }
# it { should_not allow_value('bar').for(:website_url) }
# end
#
# #### Qualifiers
#
# ##### on
#
# Use `on` if your validation applies only under a certain context.
#
# class UserProfile
# include ActiveModel::Model
# attr_accessor :birthday_as_string
#
# validates_format_of :birthday_as_string,
# with: /^(\d+)-(\d+)-(\d+)$/,
# on: :create
# end
#
# # RSpec
# describe UserProfile do
# it do
# should allow_value('2013-01-01').
# for(:birthday_as_string).
# on(:create)
# end
# end
#
# # Test::Unit
# class UserProfileTest < ActiveSupport::TestCase
# should allow_value('2013-01-01').
# for(:birthday_as_string).
# on(:create)
# end
#
# ##### with_message
#
# Use `with_message` if you are using a custom validation message.
#
# class UserProfile
# include ActiveModel::Model
# attr_accessor :state
#
# validates_format_of :state,
# with: /^(open|closed)$/,
# message: 'State must be open or closed'
# end
#
# # RSpec
# describe UserProfile do
# it do
# should allow_value('open', 'closed').
# for(:state).
# with_message('State must be open or closed')
# end
# end
#
# # Test::Unit
# class UserProfileTest < ActiveSupport::TestCase
# should allow_value('open', 'closed').
# for(:state).
# with_message('State must be open or closed')
# end
#
# Use `with_message` with a regexp to perform a partial match:
#
# class UserProfile
# include ActiveModel::Model
# attr_accessor :state
#
# validates_format_of :state,
# with: /^(open|closed)$/,
# message: 'State must be open or closed'
# end
#
# # RSpec
# describe UserProfile do
# it do
# should allow_value('open', 'closed').
# for(:state).
# with_message(/open or closed/)
# end
# end
#
# # Test::Unit
# class UserProfileTest < ActiveSupport::TestCase
# should allow_value('open', 'closed').
# for(:state).
# with_message(/open or closed/)
# end
#
# Use `with_message` with the `:against` option if the attribute the
# validation message is stored under is different from the attribute
# being validated:
#
# class UserProfile
# include ActiveModel::Model
# attr_accessor :sports_team
#
# validate :sports_team_must_be_valid
#
# private
#
# def sports_team_must_be_valid
# if sports_team !~ /^(Broncos|Titans)$/i
# self.errors.add :chosen_sports_team,
# 'Must be either a Broncos fan or a Titans fan'
# end
# end
# end
#
# # RSpec
# describe UserProfile do
# it do
# should allow_value('Broncos', 'Titans').
# for(:sports_team).
# with_message('Must be either a Broncos or Titans fan',
# against: :chosen_sports_team
# )
# end
# end
#
# # Test::Unit
# class UserProfileTest < ActiveSupport::TestCase
# should allow_value('Broncos', 'Titans').
# for(:sports_team).
# with_message('Must be either a Broncos or Titans fan',
# against: :chosen_sports_team
# )
# end
#
# @return [AllowValueMatcher]
#
def allow_value(*values)
if values.empty?
raise ArgumentError, 'need at least one argument'
else
AllowValueMatcher.new(*values)
end
end
# @private
class AllowValueMatcher
# @private
class CouldNotSetAttributeError < Shoulda::Matchers::Error
def self.create(model, attribute, expected_value, actual_value)
super(
model: model,
attribute: attribute,
expected_value: expected_value,
actual_value: actual_value
)
end
attr_accessor :model, :attribute, :expected_value, :actual_value
def message
"Expected #{model.class} to be able to set #{attribute} to #{expected_value.inspect}, but got #{actual_value.inspect} instead."
end
end
include Helpers
attr_accessor :attribute_with_message
attr_accessor :options
def initialize(*values)
self.values_to_match = values
self.options = {}
self.after_setting_value_callback = -> {}
self.validator = Validator.new
end
def for(attribute)
self.attribute_to_set = attribute
self.attribute_to_check_message_against = attribute
self
end
def on(context)
validator.context = context
self
end
def with_message(message, options={})
self.options[:expected_message] = message
self.options[:expected_message_values] = options.fetch(:values, {})
if options.key?(:against)
self.attribute_to_check_message_against = options[:against]
end
self
end
def strict
validator.strict = true
self
end
def _after_setting_value(&callback)
self.after_setting_value_callback = callback
end
def matches?(instance)
self.instance = instance
values_to_match.all? { |value| value_matches?(value) }
end
def does_not_match?(instance)
self.instance = instance
values_to_match.all? { |value| !value_matches?(value) }
end
def failure_message
"Did not expect #{expectation},\ngot#{error_description}"
end
alias failure_message_for_should failure_message
def failure_message_when_negated
"Expected #{expectation},\ngot#{error_description}"
end
alias failure_message_for_should_not failure_message_when_negated
def description
validator.allow_description(allowed_values)
end
protected
attr_reader :instance, :attribute_to_check_message_against
attr_accessor :values_to_match, :attribute_to_set, :value,
:matched_error, :after_setting_value_callback, :validator
def instance=(instance)
@instance = instance
validator.record = instance
end
def attribute_to_check_message_against=(attribute)
@attribute_to_check_message_against = attribute
validator.attribute = attribute
end
def value_matches?(value)
self.value = value
set_attribute(value)
!(errors_match? || any_range_error_occurred?)
end
def set_attribute(value)
set_attribute_ignoring_range_errors(value)
after_setting_value_callback.call
end
def set_attribute_ignoring_range_errors(value)
instance.__send__("#{attribute_to_set}=", value)
ensure_that_attribute_has_been_changed_to_or_from_nil!(value)
rescue RangeError => exception
# Have to reset the attribute so that we don't get a RangeError the
# next time we attempt to write the attribute (ActiveRecord seems to
# set the attribute to the "bad" value anyway)
reset_attribute
validator.capture_range_error(exception)
end
def reset_attribute
instance.send(:raw_write_attribute, attribute_to_set, nil)
end
def ensure_that_attribute_has_been_changed_to_or_from_nil!(expected_value)
actual_value = instance.__send__(attribute_to_set)
if expected_value.nil? != actual_value.nil?
raise CouldNotSetAttributeError.create(
instance.class,
attribute_to_set,
expected_value,
actual_value
)
end
end
def errors_match?
has_messages? && errors_for_attribute_match?
end
def has_messages?
validator.has_messages?
end
def errors_for_attribute_match?
if expected_message
self.matched_error = errors_match_regexp? || errors_match_string?
else
errors_for_attribute.compact.any?
end
end
def errors_for_attribute
validator.formatted_messages
end
def errors_match_regexp?
if Regexp === expected_message
errors_for_attribute.detect { |e| e =~ expected_message }
end
end
def errors_match_string?
if errors_for_attribute.include?(expected_message)
expected_message
end
end
def any_range_error_occurred?
validator.captured_range_error?
end
def expectation
parts = [
expected_messages_description,
"when #{attribute_to_set} is set to #{value.inspect}"
]
parts.join(' ').squeeze(' ')
end
def expected_messages_description
validator.expected_messages_description(expected_message)
end
def error_description
validator.messages_description
end
def allowed_values
if values_to_match.length > 1
"any of [#{values_to_match.map(&:inspect).join(', ')}]"
else
values_to_match.first.inspect
end
end
def expected_message
if options.key?(:expected_message)
if Symbol === options[:expected_message]
default_expected_message
else
options[:expected_message]
end
end
end
def default_expected_message
validator.expected_message_from(default_attribute_message)
end
def default_attribute_message
default_error_message(
options[:expected_message],
default_attribute_message_values
)
end
def default_attribute_message_values
defaults = {
model_name: model_name,
instance: instance,
attribute: attribute_to_check_message_against,
}
defaults.merge(options[:expected_message_values])
end
def model_name
instance.class.to_s.underscore
end
end
end
end
end