Wrote a custom validator in my rails project and want to write a test for when nil is passed into the record.
The validator code
class FutureValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if value == nil
record.errors[attribute] << "can't be nil"
elsif value <= Time.now
record.errors[attribute] << (options[:message] || "can't be in the past!")
end
end
end
Possible test
test "future validator rejects nil values" do
#obj.future = nil
assert_includes #obj.errors
end
I'd like to not only write a test that checks that assert_not #obj.valid? but actually demonstrate that the error message is being passed back. If that's asking too much I'll settle for knowing that an error message is coming back but currently my test isn't working.
It is returning
ArgumentError: wrong number of arguments (1 for 2..3)
Update
Getting close to a green test
test "future validator rejects nil values" do
#obj.future = nil
#obj.valid?
assert_includes #obj.errors.messages, {future: ["can't be nil"]}
end
Is returning
FutureValidatorTest#test_future_validator_rejects_nil_values [/Users/rwdonnard/WorkSpace/charter-pay-api/test/models/future_validator_test.rb:42]:
Expected {:future=>["can't be nil"]} to include {:future=>["can't be nil"]}.
The issue seems to be the way you test your class and most probably the exception gets raised from assert_includes. assert_includes expects at least 2 arguments with the first being a collection and the second being the object you expect to be included, in your case an array of errors and the error you expect respectively. Also your test is going to fail if you don't populate the #obj.errors collection, something that requires to call #obj.valid?
Your test should look like this:
test "future validator rejects nil values" do
#obj.future = nil
#obj.valid?
assert_includes #obj.errors.messages, { future: ["can't be nil"] }
end
This way you make sure that your model is invalid if future.nil? regardless of other validations in place.
I'd also suggest that you don't check for presence in your custom validator since Rails already provide a way for this. You could have your validator look like this:
class FutureValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if value <= Time.now
record.errors[attribute] << (options[:message] || "can't be in the past!")
end
end
end
and setup validation in your model like this:
class SomeClass < ActiveRecord::Base
validates :future, presence: true, future: true
end
You could also simplify your tests like:
test "is invalid with nil future date" do
#obj.future = nil
#obj.valid?
assert_includes #obj.errors.messages[:future], "can't be nil"
end
test "future validator rejects past dates" do
#obj.future = Date.new(1987, 1, 1)
#obj.valid?
assert_includes #obj.errors.messages[:future], "can't be in the past!"
end
Related
I have the Order model with the line for calculating total total price of products the user is ordering
before_validation :set_total!
validates :total, presence: true, numericality: { greater_than_or_equal_to: 0 }
set_total looks like this
def set_total!
self.total = products.map(&price).sum
end
On my specs I am trying to check if the total validations are implemented TDD
it { should validate_presence_of(:total) }
it { should validate_numericality_of(:total).is_greater_than_or_equal_to(0) }
Unfortunately I am receiving the following error
Failure/Error: it { should validate_presence_of(:total) }
Order did not properly validate that :total cannot be empty/falsy.
After setting :total to ‹nil› -- which was read back as
‹#<BigDecimal:5634b8b81008,'0.0',9(27)>› -- the matcher expected the
Order to be invalid, but it was valid instead.
As indicated in the message above, :total seems to be changing certain
values as they are set, and this could have something to do with why
this test is failing. If you've overridden the writer method for this
attribute, then you may need to change it to make this test pass, or
do something else entirely.
How can I fix this?
Using the validate_presence_of matcher is roughly equivalent to writing this test by hand:
describe Order do
it "fails validation when total is nil" do
order = Order.new
order.total = nil
order.validate
expect(order.errors[:total]).to include("can't be blank")
order.total = 42
expect(order.errors[:total]).not_to include("can't be blank")
end
end
If you were to run this test, you would find that this would fail. Why? Because in your model, you set total to a non-nil value when validations are performed. That's why you're getting this error.
So you don't really need the validation or the matcher, since neither one would fail.
I want a (numerical) model attribute foo to be validated as
(1) present, and
(2) be greater than or equal to 0.
Since (1) is a prerequisite to (2), in case no value is given for parameter foo, I want the validation to report only the error related to (1), and not (2).
I tried to do it like this:
validates :foo_attribute,
numericality: {greater_than_or_equal_to: 0},
presence: true
But when the given value of parameter :foo is absent, I get messages in errors that originate from both validations (1) and (2).
In such case, how can I get only the validation error related to (1) and not (2)?
Looks like internally validates() just splits it into multiple validations:
def validates(*attributes)
defaults = attributes.extract_options!.dup
validations = defaults.slice!(*_validates_default_keys)
raise ArgumentError, "You need to supply at least one attribute" if attributes.empty?
raise ArgumentError, "You need to supply at least one validation" if validations.empty?
defaults[:attributes] = attributes
validations.each do |key, options|
next unless options
key = "#{key.to_s.camelize}Validator"
begin
validator = key.include?('::') ? key.constantize : const_get(key)
rescue NameError
raise ArgumentError, "Unknown validator: '#{key}'"
end
validates_with(validator, defaults.merge(_parse_validates_options(options)))
end
end
That being the case, you would have to either write your own method or do something like:
validates_presence_of :foo_attribute
validates_numericality_of :foo_attribute, greater_than: 0, unless: Proc.new { |foo_instance| foo_instance.foo_attribute.nil? }
I am working to solve my problem to another question and I think I am close to a solution, but I have run into another problem. I am trying to copy code I found in the rails source and modify it to fit my needs:
I have created the following custom validator:
class ExistingTwoValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless Group.where(:code => value).any?
record.errors.add(attribute, :invalid, options.merge(:value => value))
record.errors.add(attribute, "custom error", options.merge(:value => value))
record.errors.add(attribute, :madeup, options.merge(:value => value))
end
end
end
If I provide a custom error message in my model:
validates :group_code, existing_two: { message: "My custom message %{value} is not okay."}
For invalid input (e.g., "www") I get the following errors:
Group code My custom message www is not okay.
Group code custom error
Group code My custom message www is not okay.
If I do not provide a custom error message:
validates :group_code, existing_two: true
For invalid input (e.g., "www") I get the following errors:
Group code is invalid
Group code custom error
Group code translation missing: en.activerecord.errors.models.XX.attributes.group_code.madeup
The first record.errors.add(attribute, :invalid, ... will allow me to override the default message in :invalid, but if I provide literal string as the second argument (e.g. record.errors.add(attribute, "custom error", it will not let me override the message.
I want to understand the logic that says if it's a symbol it can be replaced but if its a string ignore the message passed in the options. Also, is there any way to provide the custom message without using a symbol or is there a way to define the symbol without adding it to a language file somewhere?
Reviewing the following snippets from active model errors.
# File activemodel/lib/active_model/errors.rb, Line 299
def add(attribute, message = :invalid, options = {})
message = normalize_message(attribute, message, options)
if exception = options[:strict]
exception = ActiveModel::StrictValidationFailed if exception == true
raise exception, full_message(attribute, message)
end
self[attribute] << message
end
#File activemodel/lib/active_model/errors.rb, line 438
def normalize_message(attribute, message, options)
case message
when Symbol
generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
when Proc
message.call
else
message
end
end
# File activemodel/lib/active_model/errors.rb, line 412
def generate_message(attribute, type = :invalid, options = {})
type = options.delete(:message) if options[:message].is_a?(Symbol)
if #base.class.respond_to?(:i18n_scope)
defaults = #base.class.lookup_ancestors.map do |klass|
[ :"#{#base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
:"#{#base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
end
else
defaults = []
end
defaults << options.delete(:message)
defaults << :"#{#base.class.i18n_scope}.errors.messages.#{type}" if #base.class.respond_to?(:i18n_scope)
defaults << :"errors.attributes.#{attribute}.#{type}"
defaults << :"errors.messages.#{type}"
defaults.compact!
defaults.flatten!
key = defaults.shift
value = (attribute != :base ? #base.send(:read_attribute_for_validation, attribute) : nil)
options = {
default: defaults,
model: #base.model_name.human,
attribute: #base.class.human_attribute_name(attribute),
value: value
}.merge!(options)
I18n.translate(key, options)
end
You're calling errors.add, which calls the private method normalize_message, the normalise_message method takes the message and if it's a Symbol calls generate_message. In the generate_message method it ends up falling back to the default message because it is unable to find the translation in activemodel.errors.models.MODEL.attributes.ATTRIBUTE.MESSAGE or activemodel.errors.models.MODEL.MESSAGE
I strongly suggest you read the source code link I've provided as it is well documented and is very in depth.
Active Model Errors
This is how can you can do it simply
class Example < ActiveRecord::Base
validates :group_code, presence: true
validate do |example|
unless Group.where(:code => example.group_code).any?
example.errors.add(:base, "The code you have entered #{example.group_code} is not a valid code, please check with your teacher or group")
end
end
end
Of course, you can also extract the the above logic and put it into custom validator.
Note that , i am passing :base not attribute.
Basically, the interpolation is done with i18n gem.
You can see here
In my Purchase model, I have a method that calculates the tax:
def calculate_tax
if self.shipping_address.state == State.new_york
corresponding_tax = Tax.find_by(zip_code: self.shipping_address.zip_code, state_id: self.shipping_address.state_id)
if corresponding_tax
self.tax = corresponding_tax.rate * (self.subtotal + shipping)
else
#HERE !!!
self.errors[:base] << "The zip code you have entered is invalid."
puts "errors = #{self.errors.full_messages}" #<-- this prints out the error in my log, so I know it's being run
end
else
self.tax = 0.00
end
end
This method is being called within this method:
def update_all_fees!
calculate_subtotal
calculate_shipping
calculate_tax #<-- being called here
calculate_total
save!
end
However, save! is saving the record successfully. Shouldn't it be throwing an exception? How would I make it so that save! fails when calculate_tax is in the second else block?
You can add custom validation methods with the validate directive. Here's may take on the code you posted:
class Purchase < ActiveRecord::Base
validate :new_york_needs_tax_record
def update_all_fees!
calculate_subtotal
calculate_shipping
calculate_tax
calculate_total
save!
end
private
def calculate_tax
if ships_to_new_york? && corresponding_tax
self.tax = corresponding_tax.rate * (self.subtotal + shipping)
elsif !ships_to_new_york?
self.tax = 0.00
else
self.tax = nil
end
end
def ships_to_new_york?
self.shipping_address.state == State.new_york
end
def corresponding_tax
Tax.find_by(zip_code: self.shipping_address.zip_code, state_id: self.shipping_address.state_id)
end
def new_york_need_tax_record
if ships_to_new_york? && !corresponding_tax
self.errors[:base] << "The zip code you have entered is invalid."
end
end
end
Edited for historical reasons. The first response didn't cover all scenarios.
But if you need to raise the error if there are any just do:
validate :taxes_scenario
def taxes_scenario
[Add any clause here that makes your scenario invalid]
end
So you can validate the taxes scenario and made sure your error is added properly.
Simply adding an error to the error list won't make the record fail. You have to have what's called a "validation" set up. There are some wonderful guides about it here that should help you through the process.
For example in this one you will probably want to add to your Tax model the following validation:
validates :zip_code, presence: true, numericality: true
once you have the validations set up, save! should automatically spit out an error when a model fails validation
In Rails 7:
class User < ApplicationRecord
validate :must_be_a_friend
def must_be_a_friend
if friend == false
# Does NOT work anymore
errors[:base] << 'Must be friends'
# DOES work
errors.add(:base, 'Must be friends')
end
end
end
I am trying to add a custom error to an instance of my User model, but when I call valid? it is wiping the custom errors and returning true.
[99] pry(main)> u.email = "test#test.com"
"test#test.com"
[100] pry(main)> u.status = 1
1
[101] pry(main)> u.valid?
true
[102] pry(main)> u.errors.add(:status, "must be YES or NO")
[
[0] "must be YES or NO"
]
[103] pry(main)> u.errors
#<ActiveModel::Errors:[...]#messages={:status=>["must be YES or NO"]}>
[104] pry(main)> u.valid?
true
[105] pry(main)> u.errors
#<ActiveModel::Errors:[...]#messages={}>
If I use the validate method from within the model, then it works, but this specific validation is being added from within a different method (which requires params to be passed):
User
def do_something_with(arg1, arg2)
errors.add(:field, "etc") if arg1 != arg2
end
Because of the above, user.valid? is returning true even when that error is added to the instance.
In ActiveModel, valid? is defined as following:
def valid?(context = nil)
current_context, self.validation_context = validation_context, context
errors.clear
run_validations!
ensure
self.validation_context = current_context
end
So existing errors are cleared is expected. You have to put all your custom validations into some validate callbacks. Like this:
validate :check_status
def check_status
errors.add(:status, "must be YES or NO") unless ['YES', 'NO'].include?(status)
end
If you want to force your model to show the errors you could do something as dirty as this:
your_object = YourModel.new
your_object.add(:your_field, "your message")
your_object.define_singleton_method(:valid?) { false }
# later on...
your_object.valid?
# => false
your_object.errors
# => {:your_field =>["your message"]}
The define_singleton_method method can override the .valid? behaviour.
This is not a replacement for using the provided validations/framework. However, in some exceptional scenarios, you want to gracefully return an errd model. I would only use this when other alternatives aren't possible. One of the few scenarios I have had to use this approach is inside of a service object creating a model where some portion of the create fails (like resolving a dependent entity). It doesn't make sense for our domain model to be responsible for this type of validation, so we don't store it there (which is why the service object is doing the creation in the first place). However for simplicity of the API design it can be convenient to hang a domain error like 'associated entity foo not found' and return via the normal rails 422/unprocessible entity flow.
class ModelWithErrors
def self.new(*errors)
Module.new do
define_method(:valid?) { false }
define_method(:invalid?) { true }
define_method(:errors) do
errors.each_slice(2).with_object(ActiveModel::Errors.new(self)) do |(name, message), errs|
errs.add(name, message)
end
end
end
end
end
Use as some_instance.extend(ModelWithErrors.new(:name, "is gibberish", :height, "is nonsense")
create new concerns
app/models/concerns/static_error.rb
module StaticError
extend ActiveSupport::Concern
included do
validate :check_static_errors
end
def add_static_error(*args)
#static_errors = [] if #static_errors.nil?
#static_errors << args
true
end
def clear_static_error
#static_errors = nil
end
private
def check_static_errors
#static_errors&.each do |error|
errors.add(*error)
end
end
end
include the model
class Model < ApplicationRecord
include StaticError
end
model = Model.new
model.add_static_error(:base, "STATIC ERROR")
model.valid? #=> false
model.errors.messages #=> {:base=>["STATIC ERROR"]}
A clean way to achieve your needs is contexts, but if you want a quick fix, do:
#in your model
attr_accessor :with_foo_validation
validate :foo_validation, if: :with_foo_validation
def foo_validation
#code
end
#where you need it
your_object.with_foo_validation = true
your_object.valid?