Skip to content

Commit

Permalink
Fix presence matcher against an AM record
Browse files Browse the repository at this point in the history
When using the presence matcher against an ActiveModel record, check to
ensure that the model is using the Attributes API and has explicitly
declared the attribute to be a string before attempting to set the
attribute to an empty string. Otherwise, the attribute may be getting
treated as an object in some point in the validation process, and
therefore setting it to an empty string would create problems.
  • Loading branch information
mcmire committed Jul 7, 2019
1 parent 51b45b1 commit 11f4483
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 23 deletions.
20 changes: 15 additions & 5 deletions lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -310,11 +310,13 @@ def belongs_to_association_being_validated?
end

def attribute_accepts_string_values?
!association? && (
!attribute_type.respond_to?(:coder) ||
!attribute_type.coder ||
attribute_type.coder.object_class == String
)
if association?
false
elsif attribute_serializer
attribute_serializer.object_class == String
else
attribute_type.try(:type) == :string
end
end

def association?
Expand All @@ -339,6 +341,14 @@ def association_reflection
model.try(:reflect_on_association, @attribute)
end

def attribute_serializer
if attribute_type.respond_to?(:coder)
attribute_type.coder
else
nil
end
end

def attribute_type
RailsShim.attribute_type_for(model, @attribute)
end
Expand Down
7 changes: 6 additions & 1 deletion lib/shoulda/matchers/rails_shim.rb
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def secure_password_module
end

def attribute_type_for(model, attribute_name)
if model.respond_to?(:attribute_types)
if supports_full_attributes_api?(model)
model.attribute_types[attribute_name.to_s]
else
LegacyAttributeType.new(model, attribute_name)
Expand Down Expand Up @@ -188,6 +188,11 @@ def simply_generate_validation_message(
I18n.translate(primary_translation_key, translate_options)
end

def supports_full_attributes_api?(model)
defined?(::ActiveModel::Attributes) &&
model.respond_to?(:attribute_types)
end

class LegacyAttributeType
def initialize(model, attribute_name)
@model = model
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,17 @@
expect(record).to matcher
end

blank_value =
if active_model_supports_full_attributes_api?
''
else
nil
end

message = <<-MESSAGE
Expected Example to validate that :attr cannot be empty/falsy, but this
could not be proved.
After setting :attr to ‹""›, the matcher expected the Example to be
After setting :attr to ‹#{blank_value.inspect}›, the matcher expected the Example to be
invalid, but it was valid instead.
MESSAGE

Expand Down Expand Up @@ -178,6 +185,28 @@
end
end
end

context 'when the attribute has not been configured with a type' do
context 'and it is assumed to be something other than a string' do
it 'still works' do
record = active_model_object_validating_presence_of(:user) do
attribute :user

validate :validate_user_has_email, if: :user

private

def validate_user_has_email
if !user.email
errors.add(:base, 'user does not have an email')
end
end
end

expect(record).to validate_presence_of(:user)
end
end
end
end

def model_creator
Expand All @@ -196,7 +225,7 @@ def model_creator
message = <<-MESSAGE
Expected Example to validate that :attr cannot be empty/falsy, but this
could not be proved.
After setting :attr to ‹""›, the matcher expected the Example to be
After setting :attr to ‹nil›, the matcher expected the Example to be
invalid, but it was valid instead.
MESSAGE

Expand Down Expand Up @@ -735,10 +764,17 @@ def record_belonging_to(
expect(validating_presence(strict: false)).to matcher.strict
end

blank_value =
if active_model_supports_full_attributes_api?
''
else
nil
end

message = <<-MESSAGE
Expected Example to validate that :attr cannot be empty/falsy, raising a
validation exception on failure, but this could not be proved.
After setting :attr to ‹""›, the matcher expected the Example to be
After setting :attr to ‹#{blank_value.inspect}›, the matcher expected the Example to be
invalid and to raise a validation exception, but the record produced
validation errors instead.
MESSAGE
Expand Down Expand Up @@ -827,14 +863,23 @@ def foo=(value)

assertion = -> { expect(record).not_to matcher.allow_nil }

expect(&assertion).to fail_with_message(<<-MESSAGE)
if active_model_supports_full_attributes_api?
expect(&assertion).to fail_with_message(<<-MESSAGE)
Expected Example not to validate that :attr cannot be empty/falsy, but
this could not be proved.
After setting :attr to ‹""›, the matcher expected the Example to be
valid, but it was invalid instead, producing these validation errors:
* attr: ["can't be blank"]
MESSAGE
MESSAGE
else
expect(&assertion).to fail_with_message(<<-MESSAGE)
Expected Example not to validate that :attr cannot be empty/falsy, but
this could not be proved.
After setting :attr to ‹nil›, the matcher expected the Example to be
invalid, but it was valid instead.
MESSAGE
end
end
end

Expand Down Expand Up @@ -867,27 +912,52 @@ def foo=(value)
end

context 'when validating a model without a presence validator' do
it 'does not match in the positive' do
record = without_validating_presence
if active_model_supports_full_attributes_api?
it 'does not match in the positive' do
record = without_validating_presence

assertion = lambda do
expect(record).to matcher.allow_nil
end
assertion = lambda do
expect(record).to matcher.allow_nil
end

message = <<-MESSAGE
message = <<-MESSAGE
Expected Example to validate that :attr cannot be empty/falsy, but this
could not be proved.
After setting :attr to ‹""›, the matcher expected the Example to be
invalid, but it was valid instead.
MESSAGE
MESSAGE

expect(&assertion).to fail_with_message(message)
end
expect(&assertion).to fail_with_message(message)
end

it 'matches in the negative' do
record = without_validating_presence
it 'matches in the negative' do
record = without_validating_presence

expect(record).not_to matcher.allow_nil
expect(record).not_to matcher.allow_nil
end
else
it 'matches in the positive' do
record = without_validating_presence

expect(record).to matcher.allow_nil
end

it 'does not match in the negative' do
record = without_validating_presence

assertion = lambda do
expect(record).not_to matcher.allow_nil
end

message = <<-MESSAGE
Expected Example not to validate that :attr cannot be empty/falsy, but
this could not be proved.
After setting :attr to ‹nil›, the matcher expected the Example to be
invalid, but it was valid instead.
MESSAGE

expect(&assertion).to fail_with_message(message)
end
end
end
end
Expand Down

0 comments on commit 11f4483

Please sign in to comment.