shoulda-matchers 3.0.0 → 3.0.1

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +3 -1
  3. data/Gemfile +1 -2
  4. data/Gemfile.lock +8 -2
  5. data/NEWS.md +29 -1
  6. data/README.md +3 -11
  7. data/doc_config/yard/templates/default/fulldoc/html/css/global.css +17 -0
  8. data/doc_config/yard/templates/default/fulldoc/html/css/style.css +3 -4
  9. data/doc_config/yard/templates/default/layout/html/breadcrumb.erb +1 -1
  10. data/doc_config/yard/templates/default/layout/html/footer.erb +6 -0
  11. data/docs/errors/NonCaseSwappableValueError.md +111 -0
  12. data/gemfiles/4.0.0.gemfile +1 -2
  13. data/gemfiles/4.0.0.gemfile.lock +5 -2
  14. data/gemfiles/4.0.1.gemfile +1 -2
  15. data/gemfiles/4.0.1.gemfile.lock +5 -2
  16. data/gemfiles/4.1.gemfile +1 -2
  17. data/gemfiles/4.1.gemfile.lock +5 -2
  18. data/gemfiles/4.2.gemfile +1 -2
  19. data/gemfiles/4.2.gemfile.lock +5 -2
  20. data/lib/shoulda/matchers/action_controller/respond_with_matcher.rb +1 -5
  21. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +26 -4
  22. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +9 -8
  23. data/lib/shoulda/matchers/active_model/numericality_matchers/even_number_matcher.rb +14 -8
  24. data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +47 -12
  25. data/lib/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher.rb +15 -9
  26. data/lib/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher.rb +14 -7
  27. data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +16 -4
  28. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +67 -5
  29. data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +35 -0
  30. data/lib/shoulda/matchers/util.rb +2 -0
  31. data/lib/shoulda/matchers/util/word_wrap.rb +178 -0
  32. data/lib/shoulda/matchers/version.rb +1 -1
  33. data/lib/shoulda/matchers/warn.rb +1 -10
  34. data/spec/acceptance_spec_helper.rb +2 -10
  35. data/spec/doublespeak_spec_helper.rb +1 -17
  36. data/spec/spec_helper.rb +23 -0
  37. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/comparison_matcher_spec.rb +1 -1
  38. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/even_number_matcher_spec.rb +5 -2
  39. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher_spec.rb +5 -2
  40. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher_spec.rb +5 -1
  41. data/spec/unit/shoulda/matchers/active_model/validate_inclusion_of_matcher_spec.rb +91 -4
  42. data/spec/unit/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +292 -2
  43. data/spec/unit/shoulda/matchers/active_record/validate_uniqueness_of_matcher_spec.rb +16 -2
  44. data/spec/unit/shoulda/matchers/util/word_wrap_spec.rb +197 -0
  45. data/spec/unit_spec_helper.rb +1 -16
  46. data/tasks/documentation.rb +10 -17
  47. metadata +9 -2
data/gemfiles/4.2.gemfile CHANGED
@@ -7,8 +7,7 @@ gem "bundler", "~> 1.1"
7
7
  gem "pry", :github => "pry/pry"
8
8
  gem "pry-byebug"
9
9
  gem "rake", "~> 10.0"
10
- gem "rspec-core", ">= 3.2.0", "< 4"
11
- gem "rspec-expectations", ">= 3.2.0", "< 4"
10
+ gem "rspec", "~> 3.2"
12
11
  gem "yard"
13
12
  gem "redcarpet"
14
13
  gem "pygments.rb"
@@ -141,6 +141,10 @@ GEM
141
141
  rake (10.4.2)
142
142
  rdoc (4.2.0)
143
143
  redcarpet (3.3.2)
144
+ rspec (3.3.0)
145
+ rspec-core (~> 3.3.0)
146
+ rspec-expectations (~> 3.3.0)
147
+ rspec-mocks (~> 3.3.0)
144
148
  rspec-core (3.3.2)
145
149
  rspec-support (~> 3.3.0)
146
150
  rspec-expectations (3.3.1)
@@ -219,8 +223,7 @@ DEPENDENCIES
219
223
  rails (~> 4.2.0)
220
224
  rake (~> 10.0)
221
225
  redcarpet
222
- rspec-core (>= 3.2.0, < 4)
223
- rspec-expectations (>= 3.2.0, < 4)
226
+ rspec (~> 3.2)
224
227
  rspec-rails (>= 3.2.0, < 4)
225
228
  sass-rails (~> 5.0)
226
229
  sdoc (~> 0.4.0)
@@ -133,11 +133,7 @@ module Shoulda
133
133
  when :missing then 404
134
134
  when :error then 500..599
135
135
  when Symbol
136
- if defined?(::Rack::Utils::SYMBOL_TO_STATUS_CODE)
137
- ::Rack::Utils::SYMBOL_TO_STATUS_CODE[potential_symbol]
138
- else
139
- ::ActionController::Base::SYMBOL_TO_STATUS_CODE[potential_symbol]
140
- end
136
+ ::Rack::Utils::SYMBOL_TO_STATUS_CODE[potential_symbol]
141
137
  else
142
138
  potential_symbol
143
139
  end
@@ -91,9 +91,9 @@ module Shoulda
91
91
  # end
92
92
  # end
93
93
  #
94
- # * You're attempting to assert that an numeric attribute should not allow a
95
- # string that contains non-numeric characters, yet the writer method for
96
- # that attribute strips out non-numeric characters:
94
+ # * You're attempting to assert that a numeric attribute should not allow
95
+ # a string that contains non-numeric characters, yet the writer method
96
+ # for that attribute strips out non-numeric characters:
97
97
  #
98
98
  # class Foo
99
99
  # include ActiveModel::Model
@@ -326,7 +326,29 @@ module Shoulda
326
326
  attr_accessor :model, :attribute, :expected_value, :actual_value
327
327
 
328
328
  def message
329
- "Expected #{model.class} to be able to set #{attribute} to #{expected_value.inspect}, but got #{actual_value.inspect} instead."
329
+ Shoulda::Matchers.word_wrap <<-MESSAGE
330
+ The allow_value matcher attempted to set :#{attribute} on #{model.name} to
331
+ #{expected_value.inspect}, but when the attribute was read back, it
332
+ had stored #{actual_value.inspect} instead.
333
+
334
+ This creates a problem because it means that the model is behaving in a way that
335
+ is interfering with the test -- there's a mismatch between the test that was
336
+ written and test that was actually run.
337
+
338
+ There are a couple of reasons why this could be happening:
339
+
340
+ * The writer method for :#{attribute} has been overridden and contains custom
341
+ logic to prevent certain values from being set or change which values are
342
+ stored.
343
+ * ActiveRecord is typecasting the incoming value.
344
+
345
+ Regardless, the fact you're seeing this message usually indicates a larger
346
+ problem. Please file an issue on the GitHub repo for shoulda-matchers,
347
+ including details about your model and the test you've written, and we can point
348
+ you in the right direction:
349
+
350
+ https://github.com/thoughtbot/shoulda-matchers/issues
351
+ MESSAGE
330
352
  end
331
353
  end
332
354
 
@@ -20,7 +20,6 @@ module Shoulda
20
20
  @value = value
21
21
  @operator = operator
22
22
  @message = ERROR_MESSAGES[operator]
23
- @comparison_combos = comparison_combos
24
23
  @strict = false
25
24
  end
26
25
 
@@ -80,11 +79,7 @@ module Shoulda
80
79
  def submatchers
81
80
  @_submatchers ||=
82
81
  comparison_combos.map do |diff, submatcher_method_name|
83
- matcher = __send__(
84
- submatcher_method_name,
85
- (@value + diff).to_s,
86
- nil
87
- )
82
+ matcher = __send__(submatcher_method_name, diff, nil)
88
83
  matcher.with_message(@message, values: { count: @value })
89
84
  matcher
90
85
  end
@@ -127,8 +122,14 @@ module Shoulda
127
122
  end
128
123
 
129
124
  def diffs_to_compare
130
- diff = @numericality_matcher.diff_to_compare
131
- [-diff, 0, diff]
125
+ diff_to_compare = @numericality_matcher.diff_to_compare
126
+ values = [-1, 0, 1].map { |sign| @value + (diff_to_compare * sign) }
127
+
128
+ if @numericality_matcher.given_numeric_column?
129
+ values
130
+ else
131
+ values.map(&:to_s)
132
+ end
132
133
  end
133
134
 
134
135
  def comparison_expectation
@@ -6,14 +6,6 @@ module Shoulda
6
6
  class EvenNumberMatcher < NumericTypeMatcher
7
7
  NON_EVEN_NUMBER_VALUE = 1
8
8
 
9
- def initialize(attribute, options = {})
10
- @attribute = attribute
11
- @disallow_value_matcher =
12
- DisallowValueMatcher.new(NON_EVEN_NUMBER_VALUE.to_s).
13
- for(@attribute).
14
- with_message(:even)
15
- end
16
-
17
9
  def allowed_type
18
10
  'even numbers'
19
11
  end
@@ -21,6 +13,20 @@ module Shoulda
21
13
  def diff_to_compare
22
14
  2
23
15
  end
16
+
17
+ protected
18
+
19
+ def wrap_disallow_value_matcher(matcher)
20
+ matcher.with_message(:even)
21
+ end
22
+
23
+ def disallowed_value
24
+ if @numeric_type_matcher.given_numeric_column?
25
+ NON_EVEN_NUMBER_VALUE
26
+ else
27
+ NON_EVEN_NUMBER_VALUE.to_s
28
+ end
29
+ end
24
30
  end
25
31
  end
26
32
  end
@@ -1,29 +1,37 @@
1
+ require 'forwardable'
2
+
1
3
  module Shoulda
2
4
  module Matchers
3
5
  module ActiveModel
4
6
  module NumericalityMatchers
5
7
  # @private
6
8
  class NumericTypeMatcher
7
- def initialize
8
- raise NotImplementedError
9
- end
9
+ extend Forwardable
10
+
11
+ def_delegators :disallow_value_matcher, :matches?, :failure_message,
12
+ :failure_message_when_negated
10
13
 
11
- def matches?(subject)
12
- @disallow_value_matcher.matches?(subject)
14
+ def initialize(numeric_type_matcher, attribute, options = {})
15
+ @numeric_type_matcher = numeric_type_matcher
16
+ @attribute = attribute
17
+ @options = options
18
+ @message = nil
19
+ @context = nil
20
+ @strict = false
13
21
  end
14
22
 
15
23
  def with_message(message)
16
- @disallow_value_matcher.with_message(message)
24
+ @message = message
17
25
  self
18
26
  end
19
27
 
20
28
  def strict
21
- @disallow_value_matcher.strict
29
+ @strict = true
22
30
  self
23
31
  end
24
32
 
25
33
  def on(context)
26
- @disallow_value_matcher.on(context)
34
+ @context = context
27
35
  self
28
36
  end
29
37
 
@@ -35,12 +43,39 @@ module Shoulda
35
43
  raise NotImplementedError
36
44
  end
37
45
 
38
- def failure_message
39
- @disallow_value_matcher.failure_message
46
+ protected
47
+
48
+ attr_reader :attribute
49
+
50
+ def wrap_disallow_value_matcher(matcher)
51
+ raise NotImplementedError
52
+ end
53
+
54
+ def disallowed_value
55
+ raise NotImplementedError
40
56
  end
41
57
 
42
- def failure_message_when_negated
43
- @disallow_value_matcher.failure_message_when_negated
58
+ private
59
+
60
+ def disallow_value_matcher
61
+ @_disallow_value_matcher ||= begin
62
+ DisallowValueMatcher.new(disallowed_value).tap do |matcher|
63
+ matcher.for(attribute)
64
+ wrap_disallow_value_matcher(matcher)
65
+
66
+ if @message
67
+ matcher.with_message(@message)
68
+ end
69
+
70
+ if @strict
71
+ matcher.strict
72
+ end
73
+
74
+ if @context
75
+ matcher.on(@context)
76
+ end
77
+ end
78
+ end
44
79
  end
45
80
  end
46
81
  end
@@ -4,15 +4,7 @@ module Shoulda
4
4
  module NumericalityMatchers
5
5
  # @private
6
6
  class OddNumberMatcher < NumericTypeMatcher
7
- NON_ODD_NUMBER_VALUE = 2
8
-
9
- def initialize(attribute, options = {})
10
- @attribute = attribute
11
- @disallow_value_matcher =
12
- DisallowValueMatcher.new(NON_ODD_NUMBER_VALUE.to_s).
13
- for(@attribute).
14
- with_message(:odd)
15
- end
7
+ NON_ODD_NUMBER_VALUE = 2
16
8
 
17
9
  def allowed_type
18
10
  'odd numbers'
@@ -21,6 +13,20 @@ module Shoulda
21
13
  def diff_to_compare
22
14
  2
23
15
  end
16
+
17
+ protected
18
+
19
+ def wrap_disallow_value_matcher(matcher)
20
+ matcher.with_message(:odd)
21
+ end
22
+
23
+ def disallowed_value
24
+ if @numeric_type_matcher.given_numeric_column?
25
+ NON_ODD_NUMBER_VALUE
26
+ else
27
+ NON_ODD_NUMBER_VALUE.to_s
28
+ end
29
+ end
24
30
  end
25
31
  end
26
32
  end
@@ -5,13 +5,6 @@ module Shoulda
5
5
  # @private
6
6
  class OnlyIntegerMatcher < NumericTypeMatcher
7
7
  NON_INTEGER_VALUE = 0.1
8
- def initialize(attribute)
9
- @attribute = attribute
10
- @disallow_value_matcher =
11
- DisallowValueMatcher.new(NON_INTEGER_VALUE.to_s).
12
- for(attribute).
13
- with_message(:not_an_integer)
14
- end
15
8
 
16
9
  def allowed_type
17
10
  'integers'
@@ -20,6 +13,20 @@ module Shoulda
20
13
  def diff_to_compare
21
14
  1
22
15
  end
16
+
17
+ protected
18
+
19
+ def wrap_disallow_value_matcher(matcher)
20
+ matcher.with_message(:not_an_integer)
21
+ end
22
+
23
+ def disallowed_value
24
+ if @numeric_type_matcher.given_numeric_column?
25
+ NON_INTEGER_VALUE
26
+ else
27
+ NON_INTEGER_VALUE.to_s
28
+ end
29
+ end
23
30
  end
24
31
  end
25
32
  end
@@ -263,9 +263,12 @@ module Shoulda
263
263
 
264
264
  # @private
265
265
  class ValidateInclusionOfMatcher < ValidationMatcher
266
- ARBITRARY_OUTSIDE_STRING = 'shouldamatchersteststring'
266
+ ARBITRARY_OUTSIDE_STRING = 'shoulda-matchers test string'
267
267
  ARBITRARY_OUTSIDE_FIXNUM = 123456789
268
268
  ARBITRARY_OUTSIDE_DECIMAL = BigDecimal.new('0.123456789')
269
+ ARBITRARY_OUTSIDE_DATE = Date.jd(9999999)
270
+ ARBITRARY_OUTSIDE_DATETIME = DateTime.jd(9999999)
271
+ ARBITRARY_OUTSIDE_TIME = Time.at(9999999999)
269
272
  BOOLEAN_ALLOWS_BOOLEAN_MESSAGE = <<EOT
270
273
  You are using `validate_inclusion_of` to assert that a boolean column allows
271
274
  boolean values and disallows non-boolean ones. Be aware that it is not possible
@@ -447,6 +450,12 @@ EOT
447
450
  [ARBITRARY_OUTSIDE_FIXNUM]
448
451
  when :decimal
449
452
  [ARBITRARY_OUTSIDE_DECIMAL]
453
+ when :date
454
+ [ARBITRARY_OUTSIDE_DATE]
455
+ when :datetime
456
+ [ARBITRARY_OUTSIDE_DATETIME]
457
+ when :time
458
+ [ARBITRARY_OUTSIDE_TIME]
450
459
  else
451
460
  [ARBITRARY_OUTSIDE_STRING]
452
461
  end
@@ -492,9 +501,9 @@ EOT
492
501
 
493
502
  def column_type_to_attribute_type(type)
494
503
  case type
495
- when :boolean, :decimal then type
496
504
  when :integer, :float then :fixnum
497
- else :default
505
+ when :timestamp then :datetime
506
+ else type
498
507
  end
499
508
  end
500
509
 
@@ -503,7 +512,10 @@ EOT
503
512
  when true, false then :boolean
504
513
  when BigDecimal then :decimal
505
514
  when Fixnum then :fixnum
506
- else :default
515
+ when Date then :date
516
+ when DateTime then :datetime
517
+ when Time then :time
518
+ else :unknown
507
519
  end
508
520
  end
509
521
  end
@@ -179,7 +179,7 @@ module Shoulda
179
179
  #
180
180
  # ##### is_greater_than
181
181
  #
182
- # Use `is_greater_than` to test usage of tthe `:greater_than` option.
182
+ # Use `is_greater_than` to test usage of the `:greater_than` option.
183
183
  # This asserts that the attribute can take a number which is greater than
184
184
  # the given value and cannot take a number less than or equal to it.
185
185
  #
@@ -315,18 +315,19 @@ module Shoulda
315
315
  @submatchers = []
316
316
  @diff_to_compare = DEFAULT_DIFF_TO_COMPARE
317
317
  @strict = false
318
+
318
319
  add_disallow_value_matcher
319
320
  end
320
321
 
321
322
  def strict
322
- @submatchers.each(&:strict)
323
323
  @strict = true
324
+ @submatchers.each(&:strict)
324
325
  self
325
326
  end
326
327
 
327
328
  def only_integer
328
329
  prepare_submatcher(
329
- NumericalityMatchers::OnlyIntegerMatcher.new(@attribute)
330
+ NumericalityMatchers::OnlyIntegerMatcher.new(self, @attribute)
330
331
  )
331
332
  self
332
333
  end
@@ -342,14 +343,14 @@ module Shoulda
342
343
 
343
344
  def odd
344
345
  prepare_submatcher(
345
- NumericalityMatchers::OddNumberMatcher.new(@attribute)
346
+ NumericalityMatchers::OddNumberMatcher.new(self, @attribute)
346
347
  )
347
348
  self
348
349
  end
349
350
 
350
351
  def even
351
352
  prepare_submatcher(
352
- NumericalityMatchers::EvenNumberMatcher.new(@attribute)
353
+ NumericalityMatchers::EvenNumberMatcher.new(self, @attribute)
353
354
  )
354
355
  self
355
356
  end
@@ -391,6 +392,19 @@ module Shoulda
391
392
 
392
393
  def matches?(subject)
393
394
  @subject = subject
395
+
396
+ if given_numeric_column?
397
+ remove_disallow_value_matcher
398
+ end
399
+
400
+ if @submatchers.empty?
401
+ raise IneffectiveTestError.create(
402
+ model: @subject.class,
403
+ attribute: @attribute,
404
+ column_type: column_type
405
+ )
406
+ end
407
+
394
408
  first_failing_submatcher.nil?
395
409
  end
396
410
 
@@ -417,8 +431,18 @@ module Shoulda
417
431
  first_failing_submatcher.failure_message_when_negated
418
432
  end
419
433
 
434
+ def given_numeric_column?
435
+ [:integer, :float, :decimal].include?(column_type)
436
+ end
437
+
420
438
  private
421
439
 
440
+ def column_type
441
+ if @subject.class.respond_to?(:columns_hash)
442
+ @subject.class.columns_hash[@attribute.to_s].type
443
+ end
444
+ end
445
+
422
446
  def add_disallow_value_matcher
423
447
  disallow_value_matcher = DisallowValueMatcher.new(NON_NUMERIC_VALUE).
424
448
  for(@attribute).
@@ -427,8 +451,13 @@ module Shoulda
427
451
  add_submatcher(disallow_value_matcher)
428
452
  end
429
453
 
454
+ def remove_disallow_value_matcher
455
+ @submatchers.shift
456
+ end
457
+
430
458
  def prepare_submatcher(submatcher)
431
459
  add_submatcher(submatcher)
460
+
432
461
  if submatcher.respond_to?(:diff_to_compare)
433
462
  update_diff_to_compare(submatcher)
434
463
  end
@@ -476,6 +505,39 @@ module Shoulda
476
505
  arr
477
506
  end
478
507
  end
508
+
509
+ class IneffectiveTestError < Shoulda::Matchers::Error
510
+ attr_accessor :model, :attribute, :column_type
511
+
512
+ def message
513
+ Shoulda::Matchers.word_wrap <<-MESSAGE
514
+ You are attempting to use validate_numericality_of, but the attribute you're
515
+ testing, :#{attribute}, is #{a_or_an(column_type)} column. One of the things
516
+ that the numericality matcher does is to assert that setting :#{attribute} to a
517
+ string that doesn't look like #{a_or_an(column_type)} will cause your
518
+ #{humanized_model_name} to become invalid. In this case, it's impossible to make
519
+ this assertion since :#{attribute} will typecast any incoming value to
520
+ #{a_or_an(column_type)}. This means that it's already guaranteed to be numeric!
521
+ Since this matcher isn't doing anything, you can remove it from your model
522
+ tests, and in fact, you can remove the validation from your model as it isn't
523
+ doing anything either.
524
+ MESSAGE
525
+ end
526
+
527
+ private
528
+
529
+ def humanized_model_name
530
+ model.name.humanize.downcase
531
+ end
532
+
533
+ def a_or_an(next_word)
534
+ if next_word =~ /[aeiou]/
535
+ "an #{next_word}"
536
+ else
537
+ "a #{next_word}"
538
+ end
539
+ end
540
+ end
479
541
  end
480
542
  end
481
543
  end