shoulda-matches error when using model setters before validarion - ruby-on-rails

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.

Related

Rails validations - cutom messages are not applied

I have a Ride model with price float field and validation of the precision. I want to display my own custom error message when the validation fails but it doesn't work.
According to Rails Gudes "the :message option lets you specify the message that will be added to the errors collection when validation fails. When this option is not used, Active Record will use the respective default error message for each validation helper. The :message option accepts a String or Proc."
I do it exactly as in the example there and it does not work.
Rails guides
validates :age, numericality: { message: "%{value} seems wrong" }
My example
validates :price, numericality: { message: "Invalid price. Max 2 digits after period"}, format: { with: /\A\d{1,4}(.\d{0,2})?\z/ }
spec/models/ride_spec.rb
context 'with more than 2 digits after period' do
let(:price) { 29.6786745 }
it 'the price is invalid' do
expect(subject.save).to be_falsy
expect(subject).not_to be_persisted
puts subject.errors.full_messages.last # "Price is invalid"
end
end
What am I doing wrong?
Update
This is what I've learned so far.
I have set the price to be empty in the test and it now shows the error message that I want.
context 'with more than 2 digits after period' do
let(:price) { '' }
it 'the price is invalid' do
expect(subject.save).to be_falsy
expect(subject).not_to be_persisted
puts subject.errors.full_messages.last # "Price Invalid price. Max 2 digits after period"
end
end
Conslusion: it works for 'presence' validation, not for numericality validation, which is very confusing as the docs say clearly that you validate numericality, not presence. Am I right? Is this an error or deliberate?
I think where you are going wrong is expecting that numericality accepts a validation option format. Referring to the active record guides there is no option for format.
Seeing that you have called this price it seems you want to keep the precision to 2 decimal places so you can store the dollar value of something. The proper type for this is a decimal with scale: 2, or something I've had success with in the past is storing the price as an integer price_in_cents.
context 'with more than 2 digits after period' do
let(:price) { 123.333 }
it 'rounds to 2 decimal places' do
expect(subject.save).to eq true
expect(subject.reload.price).to eq 123.34
end
end
I figured this out, there are two validations: format validation and numericality validation. I did not add message to format validation, so when it fails I get the standard message
validates :price, format: { with: /\A\d{1,4}(.\d{0,2})?\z/, message: 'Invalid price. Max 2 digits after period'}, numericality: { message: 'is not a number' }

Ruby on Rails with RSpec and shoulda-matcher validate_inclusion_of giving NoMethodError

I'm having problems with the validate_inclusion_of matcher when writing a spec.
My current specs are to do with users and user groups. A user has a user group id.
I want to check that the user group id of a user is actually in the list of current user group ids.
At the moment my spec is basically:
describe 'company_user_group list of valid IDs' do
let!(:company_user_group_everything) { FactoryGirl.create(:company_user_group_everything) }
let!(:company_user_group_nothing) { FactoryGirl.create(:company_user_group_nothing) }
it '' do
#company_user = FactoryGirl.create(:company_user, id: 1)
CompanyUser.current_id = #company_user.id
is_expected.to validate_inclusion_of(:company_user_group_id).in_array(CompanyUserGroup.full_list_of_ids)
end
end
but the error I get is:
1) CompanyUser validations company_user_group list of valid IDs should validate that :company_user_group_id is either ‹2› or ‹3›
Failure/Error: is_expected.to validate_inclusion_of(:company_user_group_id).in_array(CompanyUserGroup.full_list_of_ids)
NoMethodError:
undefined method `attribute_setter' for nil:NilClass
I have tried various different things and debugged using byebug etc. but nothing is working for me.
e.g.
Adding in
#company_user_group_id = #company_user.company_user_group_id
and changing the is_expected.to line to
is_expected.to validate_inclusion_of(#company_user_group_id).in_array(CompanyUserGroup.full_list_of_ids)
I get the following error
1) CompanyUser validations company_user_group list of valid IDs should validate that :8 is either ‹8› or ‹9›
Failure/Error: is_expected.to validate_inclusion_of(#company_user_group_id).in_array(CompanyUserGroup.full_list_of_ids)
Shoulda::Matchers::ActiveModel::AllowValueMatcher::AttributeDoesNotExistError:
The matcher attempted to set :8 on the CompanyUser to 8, but that
attribute does not exist
So it seems that the group id to check is valid (e.g. 8) and the array is valid (e.g. 8 and 9) but the matching isn't working.
Any help much appreciated!
Some of the company user model
# This model holds User identities for internal staff. This model is
# primarily intended for use in the xxxx application.
class CompanyUser LT ActiveRecord::Base
acts_as_authentic do |c|
c.merge_validates_uniqueness_of_email_field_options case_sensitive: false
c.merge_validates_length_of_password_field_options minimum: 8
if Rails.env.production?
c.logged_in_timeout = 30.minutes
else
c.logged_in_timeout = 90.minutes
end
end
# Constants
# relationships
belongs_to :company_user_group
# validations
validates :first_name, presence: true, length: { maximum: 20 }
validates :last_name, presence: true, length: { maximum: 20 }
validates :company_user_group_id, presence: true, numericality: { only_integer: true, greater_than: 0 }
validates :company_user_group_id, inclusion: { in: CompanyUserGroup.full_list_of_ids }, unless: 'Rails.env.test?'
validate :check_sys_admin_permission
If the "unless: 'Rails.env.test?'" bit is removed, most of the specs fail for some unknown reason. Just mentioned in case it is relevant.
and the following method from the Company Group model
# class methods
def self.full_list_of_ids
CompanyUserGroup.all.pluck(:id)
end
I think you want something like this:
it '' do
#company_user = FactoryGirl.create(:company_user, id: 1)
#company_user.company_user_group = company_user_group_everything
expect(#company_user).to validate_inclusion_of(:company_user_group_id).in_array(CompanyUserGroup.full_list_of_ids)
end
You might not even need to set the company user gropu of the company user.
But you've got
validates :company_user_group_id, inclusion: { in: CompanyUserGroup.full_list_of_ids }, unless: 'Rails.env.test?'
Which surely means it would fail because you don't do that in the test environment
I have figured it out. For some reason the validate_inclusion_of works when it is within a let! block.
It doesn't matter what argument is sent to the let helper method, just that one is.
Still don't know WHY this fixes it though, if anybody can enlighten me!?
So, the following spec now passes
describe 'company_user_group list of valid IDs' do
let!(:company_user_group_everything) { FactoryGirl.create(:company_user_group_everything) }
let!(:company_user_group_nothing) { FactoryGirl.create(:company_user_group_nothing) }
let!(:temp) do
it {is_expected.to validate_inclusion_of(:company_user_group_id).in_array(CompanyUserGroup.full_list_of_ids)}
end
end

How to disable other validations in case more important validation error is caught

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? }

Rails uniqueness validation failing

I have a SapEvento model which belongs to SapIntegracao model. On SapEvento, I have the following validation:
validates :params_operacao, uniqueness: true, if: :check_params
def check_params
self.sap_integracao_log.recebimento
end
I'm doing that to prevent duplicate requests on a soap integration (sometimes the same xml comes multiple times). So the params I receive are saved on that :params_operacao attribute, as a string. But the problem is the validation in saving. When I receive the XML and create the SapEvento object by doing
evento_erro = sap_integracao_log.sap_eventos.create(
evento_tipo: SapEvento.get_evento_tipo("Erro na Integração"),
params_operacao: params.to_s,
erro_descricao: (!transporte ? "Transporte não encontrado" : erro),
reenviado: false,
operacao: operacao
)
it doesn't validate and permits to create another object, even if already exists an object with same :params_operacao value.
I debugged on check_params method:
logger.debug "recebimento #{self.sap_integracao_log.recebimento}"
logger.debug "count #{SapEvento.where(params_operacao: self.params_operacao).count}"
and recebimento is true and the count is bigger than 0... so it shouldn't permitt, right?
I also tried some other syntaxes, like:
validates :params_operacao, uniqueness: true, if: "self.sap_integracao_log.recebimento"
but none of these worked. Any ideas?
EDIT:
I also tried, on console, take the last SapEvento.params_operacao and create a new SapEvento doing SapEvento.new(params_operacao: last_evento_params_operacao) and save. It also doesn't give me any errors...

How do I use the value of an attribute within a model? Ruby on Rails

Basically, I have a model, Degree, and it has three attributes: degree_type, awarded_by, and date_awarded.
There are two arrays of values that should be valid for awarded_by. The two valid values for degree_type are "one" and "two", and the valid values for awarded_by depend on "one" and "two".
If degree_type is "one" (has a value of "one", that a user would put in), I want the valid values for awarded_by to be array_one. If degree_type has a value of "two", I want the valid values for awarded_by to be array_two.
Here is the code so far:
class Degree < ActiveRecord::Base
extend School
validates :degree_type, presence: true,
inclusion: { in: ["one",
"two"],
message: "is not a valid degree type"
}
validates :awarded_by, presence: true,
inclusion: { in: Degree.schools(awarded_by_type) }
end
Degree.schools outputs an array depending on the degree type, so Degree.schools("one") would return array_one, where
array_one = ['school01', 'school02'...]
My problem is, I don't know how to access the value of degree_type within the model.
What I tried below doesn't work:
validates :awarded_by, presence: true,
inclusion: { in: Degree.schools(:degree_type) }
I tried using before_type_cast but I was either using it incorrectly or there was another problem, as I couldn't get that to work either.
When I test this I get:
An object with the method #include? or a proc, lambda or symbol is required, and must be supplied as the :in (or :within) option of the configuration hash
Help me out? :) If any more info is needed, let me know.
EDIT: To add to this, I double checked it wasn't my Degree.schools method acting up - if I go into the rails console and try Degree.schools("one") or Degree.schools("two") I do get the array I should get. :)
EDIT again: When I tried #Jordan's answer, I got errors in the cases where the awarded_by was incorrect because in those cases, valid_awarded_by_values was nil and there is no include? method for a nil object. Therefore I added an if statement checking for whether valid_awarded_by_values was nil or not (so as to return if it was), and that solved the problem!
I put this inside the method, before the unless statement and after the valid_awarded_by_values declaration:
if valid_awarded_by_values.nil?
error_msg = "is not a valid awarded_by"
errors.add(:awarded_by, error_msg)
return
end
The easiest way will be to write a custom validation method, as described in the Active Record Validations Rails Guide.
In your case, it might look something like this:
class Degree < ActiveRecord::Base
validate :validate_awarded_by_inclusion_dependent_on_degree_type
# ...
def validate_awarded_by_inclusion_dependent_on_degree_type
valid_awarded_by_values = Degree.schools(degree_type)
unless valid_awarded_by_values.include?(awarded_by)
error_msg = "must be " << valid_awarded_by_values.to_sentence(two_words_connector: ' or ', last_word_connector: ', or ')
errors.add(:awarded_by, error_msg)
end
end
end

Resources