thoughtbot--shoulda-matchers/lib/shoulda/matchers/active_model/validate_presence_of_matche...

176 lines
5.4 KiB
Ruby
Raw Normal View History

module Shoulda
2010-12-15 22:34:19 +00:00
module Matchers
module ActiveModel
# The `validate_presence_of` matcher tests usage of the
# `validates_presence_of` validation.
#
# class Robot
# include ActiveModel::Model
# attr_accessor :arms
#
# validates_presence_of :arms
# end
#
# # RSpec
# RSpec.describe Robot, type: :model do
# it { should validate_presence_of(:arms) }
# end
#
# # Minitest (Shoulda)
# class RobotTest < ActiveSupport::TestCase
# should validate_presence_of(:arms)
# end
#
# #### Caveats
#
# Under Rails 4 and greater, if your model `has_secure_password` and you
# are validating presence of the password using a record whose password
# has already been set prior to calling the matcher, you will be
# instructed to use a record whose password is empty instead.
#
# For example, given this scenario:
#
# class User < ActiveRecord::Base
# has_secure_password validations: false
#
# validates_presence_of :password
# end
#
# RSpec.describe User, type: :model do
# subject { User.new(password: '123456') }
#
# it { should validate_presence_of(:password) }
# end
#
# the above test will raise an error like this:
#
# The validation failed because your User model declares
# `has_secure_password`, and `validate_presence_of` was called on a
# user which has `password` already set to a value. Please use a user
# with an empty `password` instead.
#
# This happens because `has_secure_password` itself overrides your model
# so that it is impossible to set `password` to nil. This means that it is
# impossible to test that setting `password` to nil places your model in
# an invalid state (which in turn means that the validation itself is
# unnecessary).
#
# #### Qualifiers
#
# ##### on
#
# Use `on` if your validation applies only under a certain context.
#
# class Robot
# include ActiveModel::Model
# attr_accessor :arms
#
# validates_presence_of :arms, on: :create
# end
#
# # RSpec
# RSpec.describe Robot, type: :model do
# it { should validate_presence_of(:arms).on(:create) }
# end
#
# # Minitest (Shoulda)
# class RobotTest < ActiveSupport::TestCase
# should validate_presence_of(:arms).on(:create)
# end
#
# ##### with_message
#
# Use `with_message` if you are using a custom validation message.
#
# class Robot
# include ActiveModel::Model
# attr_accessor :legs
#
# validates_presence_of :legs, message: 'Robot has no legs'
# end
#
# # RSpec
# RSpec.describe Robot, type: :model do
# it do
# should validate_presence_of(:legs).
# with_message('Robot has no legs')
# end
# end
2010-12-15 22:34:19 +00:00
#
# # Minitest (Shoulda)
# class RobotTest < ActiveSupport::TestCase
# should validate_presence_of(:legs).
# with_message('Robot has no legs')
# end
2010-12-15 22:34:19 +00:00
#
# @return [ValidatePresenceOfMatcher]
2010-12-15 22:34:19 +00:00
#
def validate_presence_of(attr)
ValidatePresenceOfMatcher.new(attr)
end
# @private
class ValidatePresenceOfMatcher < ValidationMatcher
def initialize(attribute)
super
@expected_message = :blank
2010-12-15 22:34:19 +00:00
end
def matches?(subject)
super(subject)
if secure_password_being_validated?
ignore_interference_by_writer.default_to(when: :blank?)
disallows_and_double_checks_value_of!(blank_value, @expected_message)
else
disallows_original_or_typecast_value?(blank_value, @expected_message)
end
2010-12-15 22:34:19 +00:00
end
def simple_description
"validate that :#{@attribute} cannot be empty/falsy"
2010-12-15 22:34:19 +00:00
end
private
def secure_password_being_validated?
defined?(::ActiveModel::SecurePassword) &&
@subject.class.ancestors.include?(::ActiveModel::SecurePassword::InstanceMethodsOnActivation) &&
@attribute == :password
end
def disallows_and_double_checks_value_of!(value, message)
disallows_value_of(value, message)
allow_value: pre-set attributes before validation While attempting to add support for `ignoring_interference_by_writer` to the confirmation matcher, I was noticing that there are two attributes we are concerned with: the attribute under test, and the confirmation attribute -- for instance, `password` and `password_confirmation`. The way that the matcher works, `password_confirmation` is set first on the record before `password` is set, and then the whole record is validated. This is fine, but I also noticed that `allow_value` has a specific way of setting attributes -- not only does it check whether the attribute being set exists and fail properly if it is does not, but it also raises a CouldNotSetAttribute error if the attribute changes incoming values. This logic needs to be performed on both `password_confirmation` as well as `password`. With that in mind, `allow_value` now supports a `values_to_preset=` writer method which allows one to assign additional attributes unrelated to the one being tested prior to validation. This will be used by the confirmation matcher in a future commit. This means that `allow_value` now operates in two steps: 1. Set attributes unrelated to the test, raising an error if any of the attributes do not exist on the model. 2. Set the attribute under test to one or more values, raising an error if the attribute does not exist, then running validations on the record, failing with an appropriate error message if the validations fail. Note that the second step is similar to the first, although there are more things involved. To that end, `allow_value` has been completely refactored so that the logic for setting and validating attributes happens in other places. Specifically, the core logic to set an attribute (and capture the results) is located in a new AttributeSetter class. Also, the CouldNotSetAttributeError class has been moved to a namespace and renamed to AttributeChangedValueError. Finally, this commit fixes DisallowValueMatcher so that it is the true opposite of AllowValueMatcher: DVM#matches? calls AVM#does_not_match? and DVM#does_not_match? calls AVM#matches?.
2015-12-19 17:11:01 +00:00
rescue ActiveModel::AllowValueMatcher::AttributeChangedValueError
raise ActiveModel::CouldNotSetPasswordError.create(@subject.class)
end
def disallows_original_or_typecast_value?(value, message)
disallows_value_of(blank_value, @expected_message)
end
2010-12-15 22:34:19 +00:00
def blank_value
if collection?
[]
else
nil
end
end
def collection?
2012-04-24 22:01:50 +00:00
if reflection
2010-12-15 22:34:19 +00:00
[:has_many, :has_and_belongs_to_many].include?(reflection.macro)
else
false
end
end
2012-04-24 22:01:50 +00:00
def reflection
@subject.class.respond_to?(:reflect_on_association) &&
@subject.class.reflect_on_association(@attribute)
end
2010-12-15 22:34:19 +00:00
end
end
end
end