Skip to content

Commit

Permalink
Fix inclusion matcher w/ date & datetime attrs
Browse files Browse the repository at this point in the history
Why:

* The inclusion matcher (when used with the `in_array` qualifier) makes
  the assertion that when the attribute is set to a value that is
  outside the given array, the record in question is invalid. The issue
  is that when used with a date or datetime attribute, the arbitrary
  value the matcher chose was a string. This was getting typecast and so
  the matcher was throwing a CouldNotSetAttributeError.

To satisfy the above:

* If the column is a date, use a Date for the arbitrary value
* If the column is a datetime, use a DateTime for the arbitrary value
* If the column is a time, use a Time for the arbitrary value
  • Loading branch information
mcmire committed Oct 4, 2015
1 parent 5a5af10 commit 8fa97b4
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 8 deletions.
7 changes: 7 additions & 0 deletions NEWS.md
@@ -1,3 +1,10 @@
# HEAD

### Bug fixes

* Fix `validate_inclusion_of` + `in_array` when used against a date or datetime
attribute so that it does not raise a CouldNotSetAttributeError.

# 3.0.0

### Backward-incompatible changes
Expand Down
20 changes: 16 additions & 4 deletions lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb
Expand Up @@ -263,9 +263,12 @@ def validate_inclusion_of(attr)

# @private
class ValidateInclusionOfMatcher < ValidationMatcher
ARBITRARY_OUTSIDE_STRING = 'shouldamatchersteststring'
ARBITRARY_OUTSIDE_STRING = 'shoulda-matchers test string'
ARBITRARY_OUTSIDE_FIXNUM = 123456789
ARBITRARY_OUTSIDE_DECIMAL = BigDecimal.new('0.123456789')
ARBITRARY_OUTSIDE_DATE = Date.jd(9999999)
ARBITRARY_OUTSIDE_DATETIME = DateTime.jd(9999999)
ARBITRARY_OUTSIDE_TIME = Time.at(9999999999)
BOOLEAN_ALLOWS_BOOLEAN_MESSAGE = <<EOT
You are using `validate_inclusion_of` to assert that a boolean column allows
boolean values and disallows non-boolean ones. Be aware that it is not possible
Expand Down Expand Up @@ -447,6 +450,12 @@ def outside_values
[ARBITRARY_OUTSIDE_FIXNUM]
when :decimal
[ARBITRARY_OUTSIDE_DECIMAL]
when :date
[ARBITRARY_OUTSIDE_DATE]
when :datetime
[ARBITRARY_OUTSIDE_DATETIME]
when :time
[ARBITRARY_OUTSIDE_TIME]
else
[ARBITRARY_OUTSIDE_STRING]
end
Expand Down Expand Up @@ -492,9 +501,9 @@ def attribute_column

def column_type_to_attribute_type(type)
case type
when :boolean, :decimal then type
when :integer, :float then :fixnum
else :default
when :timestamp then :datetime
else type
end
end

Expand All @@ -503,7 +512,10 @@ def value_to_attribute_type(value)
when true, false then :boolean
when BigDecimal then :decimal
when Fixnum then :fixnum
else :default
when Date then :date
when DateTime then :datetime
when Time then :time
else :unknown
end
end
end
Expand Down
Expand Up @@ -75,7 +75,7 @@ def expect_to_match_on_values(builder, values, &block)
end
end

context "against a float attribute" do
context 'against a float attribute' do
it_behaves_like 'it supports in_array',
possible_values: [1.0, 2.0, 3.0, 4.0, 5.0],
zero: 0.0,
Expand All @@ -97,7 +97,7 @@ def add_outside_value_to(values)
end
end

context "against a decimal attribute" do
context 'against a decimal attribute' do
it_behaves_like 'it supports in_array',
possible_values: [1.0, 2.0, 3.0, 4.0, 5.0].map { |number|
BigDecimal.new(number.to_s)
Expand All @@ -121,6 +121,72 @@ def add_outside_value_to(values)
end
end

context 'against a date attribute' do
today = Date.today

it_behaves_like 'it supports in_array',
possible_values: (1..5).map { |n| today + n },
reserved_outside_value: described_class::ARBITRARY_OUTSIDE_DATE

it_behaves_like 'it supports in_range',
possible_values: (today .. today + 5)

define_method :build_object do |options = {}, &block|
build_object_with_generic_attribute(
options.merge(column_type: :date, value: today),
&block
)
end

def add_outside_value_to(values)
values + [values.last + 1]
end
end

context 'against a datetime attribute (using DateTime)' do
now = DateTime.now

it_behaves_like 'it supports in_array',
possible_values: (1..5).map { |n| now + n },
reserved_outside_value: described_class::ARBITRARY_OUTSIDE_DATETIME

it_behaves_like 'it supports in_range',
possible_values: (now .. now + 5)

define_method :build_object do |options = {}, &block|
build_object_with_generic_attribute(
options.merge(column_type: :datetime, value: now),
&block
)
end

def add_outside_value_to(values)
values + [values.last + 1]
end
end

context 'against a datetime attribute (using Time)' do
now = Time.now

it_behaves_like 'it supports in_array',
possible_values: (1..5).map { |n| now + n },
reserved_outside_value: described_class::ARBITRARY_OUTSIDE_TIME

it_behaves_like 'it supports in_range',
possible_values: (now .. now + 5)

define_method :build_object do |options = {}, &block|
build_object_with_generic_attribute(
options.merge(column_type: :time, value: now),
&block
)
end

def add_outside_value_to(values)
values + [values.last + 1]
end
end

context 'against a string attribute' do
it_behaves_like 'it supports in_array',
possible_values: %w(foo bar baz),
Expand Down Expand Up @@ -270,7 +336,7 @@ def add_outside_value_to(values)
end

if zero
it 'matches when one of the given values is a 0' do
it 'matches when one of the given values is a zero' do
valid_values = possible_values + [zero]
builder = build_object_allowing(valid_values)
expect_to_match_on_values(builder, valid_values)
Expand Down Expand Up @@ -527,7 +593,6 @@ def build_object(options = {}, &block)
end
end


def build_object_with_generic_attribute(options = {}, &block)
attribute_name = :attr
column_type = options.fetch(:column_type)
Expand Down

0 comments on commit 8fa97b4

Please sign in to comment.