shoulda-matchers 3.0.0 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
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