Validate enum column is an integer in `define_enum_for`

If your ActiveRecord model stores its `enum` data in a non-integer
column, ActiveRecord will save the data without error. However, when you
access the attribute on the record after saving, AR will look for the
string to what it expects to be a list of numbers and return `nil`
rather than the mapped value.

This change adds a third criterion to the `define_enum_for` matcher,
verifying that the underlying database column has a `sql_type` of
`"integer"`.

Fix #827.
This commit is contained in:
Geoff Harcourt 2015-11-02 09:26:02 -05:00 committed by Elliot Winkler
parent 2aee2f4168
commit 68dd70a23d
3 changed files with 104 additions and 45 deletions

View File

@ -46,6 +46,9 @@
* Update `validate_numericality_of` so that it no longer raises an
IneffectiveTestError if used against a numeric column.
* Add an additional check to `define_enum_for` to ensure that the column that
underlies the enum attribute you're testing is an integer column.
# 3.0.1
### Bug fixes

View File

@ -63,8 +63,8 @@ module Shoulda
end
def matches?(subject)
@model = subject
enum_defined? && enum_values_match?
@record = subject
enum_defined? && enum_values_match? && column_type_is_integer?
end
def failure_message
@ -84,15 +84,17 @@ module Shoulda
desc << " with #{options[:expected_enum_values]}"
end
desc << " and store the value in a column with an integer type"
desc
end
protected
attr_reader :model, :attribute_name, :options
attr_reader :record, :attribute_name, :options
def expectation
"#{model.class.name} to #{description}"
"#{model.name} to #{description}"
end
def expected_enum_values
@ -100,7 +102,7 @@ module Shoulda
end
def actual_enum_values
model.class.send(attribute_name.to_s.pluralize)
model.send(attribute_name.to_s.pluralize)
end
def enum_defined?
@ -111,6 +113,18 @@ module Shoulda
expected_enum_values.empty? || actual_enum_values == expected_enum_values
end
def column_type_is_integer?
column.type == :integer
end
def column
model.columns_hash[attribute_name.to_s]
end
def model
record.class
end
def hashify(value)
if value.nil?
return {}

View File

@ -6,10 +6,12 @@ describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do
it 'rejects' do
record = record_with_array_values
plural_enum_attribute = enum_attribute.to_s.pluralize
message = "Expected #{record.class} to define :#{plural_enum_attribute} as an enum"
assertion = -> {
message = "Expected #{record.class} to define :#{plural_enum_attribute} as an enum and store the value in a column with an integer type"
assertion = lambda do
expect(record).to define_enum_for(plural_enum_attribute)
}
end
expect(&assertion).to fail_with_message(message)
end
end
@ -19,10 +21,13 @@ describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do
model = define_model :example do
def self.statuses; end
end
message = "Expected #{model} to define :statuses as an enum"
assertion = -> {
message = "Expected #{model} to define :statuses as an enum and store the value in a column with an integer type"
assertion = lambda do
expect(model.new).to define_enum_for(:statuses)
}
end
expect(&assertion).to fail_with_message(message)
end
end
@ -33,19 +38,38 @@ describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do
end
it "rejects a record where the attribute is not defined as an enum" do
message = "Expected #{record_with_array_values.class} to define :#{non_enum_attribute} as an enum"
message = "Expected #{record_with_array_values.class} to define :#{non_enum_attribute} as an enum and store the value in a column with an integer type"
expect do
expect(record_with_array_values).to define_enum_for(non_enum_attribute)
end.to fail_with_message(message)
assertion = lambda do
expect(record_with_array_values).
to define_enum_for(non_enum_attribute)
end
expect(&assertion).to fail_with_message(message)
end
it "rejects a record where the attribute is not defined as an enum with should not" do
message = "Did not expect #{record_with_array_values.class} to define :#{enum_attribute} as an enum"
message = "Did not expect #{record_with_array_values.class} to define :#{enum_attribute} as an enum and store the value in a column with an integer type"
expect do
expect(record_with_array_values).to_not define_enum_for(enum_attribute)
end.to fail_with_message(message)
assertion = lambda do
expect(record_with_array_values).
not_to define_enum_for(enum_attribute)
end
expect(&assertion).to fail_with_message(message)
end
context 'if the column storing the attribute is not an integer type' do
it 'rejects' do
record = record_with_array_values(column_type: :string)
message = "Expected #{record.class} to define :statuses as an enum and store the value in a column with an integer type"
assertion = lambda do
expect(record).to define_enum_for(:statuses)
end
expect(&assertion).to fail_with_message(message)
end
end
end
@ -57,19 +81,26 @@ describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do
end
it "accepts a record where the attribute is not defined as an enum" do
message = %{Expected #{record_with_array_values.class} to define :#{non_enum_attribute} as an enum with ["open", "close"]}
message = %{Expected #{record_with_array_values.class} to define :#{non_enum_attribute} as an enum with ["open", "close"] and store the value in a column with an integer type}
expect do
expect(record_with_array_values).to define_enum_for(non_enum_attribute).with(["open", "close"])
end.to fail_with_message(message)
assertion = lambda do
expect(record_with_array_values).
to define_enum_for(non_enum_attribute).with(['open', 'close'])
end
expect(&assertion).to fail_with_message(message)
end
it "accepts a record where the attribute is defined as an enum but the enum values do not match" do
message = %{Expected #{record_with_array_values.class} to define :#{enum_attribute} as an enum with ["open", "close"]}
message = %{Expected #{record_with_array_values.class} to define :#{enum_attribute} as an enum with ["open", "close"] and store the value in a column with an integer type}
expect do
expect(record_with_array_values).to define_enum_for(enum_attribute).with(["open", "close"])
end.to fail_with_message(message)
assertion = lambda do
expect(record_with_array_values).
to define_enum_for(enum_attribute).
with(["open", "close"])
end
expect(&assertion).to fail_with_message(message)
end
end
@ -83,20 +114,27 @@ describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do
end
it "accepts a record where the attribute is defined as an enum but the enum values do not match" do
message = %{Expected #{record_with_hash_values.class} to define :#{enum_attribute} as an enum with {:active=>5, :archived=>10}}
message = %{Expected #{record_with_hash_values.class} to define :#{enum_attribute} as an enum with {:active=>5, :archived=>10} and store the value in a column with an integer type}
expect do
expect(record_with_hash_values).to define_enum_for(enum_attribute).with(active: 5, archived: 10)
end.to fail_with_message(message)
assertion = lambda do
expect(record_with_hash_values).
to define_enum_for(enum_attribute).
with(active: 5, archived: 10)
end
expect(&assertion).to fail_with_message(message)
end
it "rejects a record where the attribute is not defined as an enum" do
message = %{Expected #{record_with_hash_values.class} to define :record_with_hash_values as an enum with {:active=>5, :archived=>10}}
message = %{Expected #{record_with_hash_values.class} to define :record_with_hash_values as an enum with {:active=>5, :archived=>10} and store the value in a column with an integer type}
expect do
expect(record_with_hash_values)
.to define_enum_for(:record_with_hash_values).with(active: 5, archived: 10)
end.to fail_with_message(message)
assertion = lambda do
expect(record_with_hash_values).
to define_enum_for(:record_with_hash_values).
with(active: 5, archived: 10)
end
expect(&assertion).to fail_with_message(message)
end
end
end
@ -109,18 +147,22 @@ describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do
:condition
end
def record_with_array_values
_enum_attribute = enum_attribute
define_model :record_with_array_values do
enum(_enum_attribute => %w(published unpublished draft))
end.new
def record_with_array_values(column_type: :integer)
model = define_model(
:record_with_array_values,
enum_attribute => { type: column_type },
)
model.enum(enum_attribute => ['published', 'unpublished', 'draft'])
model.new
end
def record_with_hash_values
_enum_attribute = enum_attribute
define_model :record_with_hash_values do
enum(_enum_attribute => { active: 0, archived: 1 })
end.new
model = define_model(
:record_with_hash_values,
enum_attribute => { type: :integer },
)
model.enum(enum_attribute => { active: 0, archived: 1 })
model.new
end
end
end